Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Pre-commit hooks configuration
# See https://pre-commit.com for more information
repos:
- repo: https://github.com/psf/black
rev: 24.10.0
hooks:
- id: black
language_version: python3
args: ['--line-length=119']

- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.43.0
hooks:
- id: markdownlint
args: ['--config', '.markdownlint.json']
31 changes: 27 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ contributors.
pip install -e ".[dev]"
```

4. Create a branch for your changes:
4. Install pre-commit hooks:

```bash
pre-commit install
```

This will automatically run black (Python formatter with line-length=119) and markdownlint
before each commit.

5. Create a branch for your changes:

```bash
git checkout -b feature/your-feature-name
Expand All @@ -50,18 +59,30 @@ pytest --cov=bad_path --cov-report=term-missing

### Code Quality

Format code with black (line-length=119):

```bash
black .
```

Check code with ruff:

```bash
ruff check .
```

Format code (if needed):
Format code with ruff (if needed):

```bash
ruff format .
```

Run pre-commit hooks manually on all files:

```bash
pre-commit run --all-files
```

### Building Documentation

Build the documentation locally:
Expand All @@ -79,8 +100,9 @@ The built documentation will be in `docs/_build/html/`.

- Follow PEP 8 style guide
- Use Python 3.10+ features (type unions with |, match/case statements)
- Line length: 100 characters (configured in pyproject.toml)
- Use ruff for linting and formatting
- Line length: 119 characters for black formatting, 100 characters for ruff
- Use black for automatic code formatting
- Use ruff for linting and additional formatting

### Docstrings

Expand Down Expand Up @@ -116,6 +138,7 @@ The built documentation will be in `docs/_build/html/`.
3. **Test Your Changes**:
- Run the full test suite
- Check code quality with ruff
- Format code with black (or run pre-commit hooks)
- Build documentation to check for errors
- Test on multiple platforms if possible

Expand Down
24 changes: 6 additions & 18 deletions bad_path/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,7 @@ def __init__(
self._user_paths_ok = user_paths_ok
self._not_writeable = not_writeable
case _:
raise ValueError(
f"Invalid mode '{mode}'. Must be None, 'read', or 'write'."
)
raise ValueError(f"Invalid mode '{mode}'. Must be None, 'read', or 'write'.")

# Handle cwd_only flag (independent of mode)
self._cwd_only = cwd_only
Expand Down Expand Up @@ -466,9 +464,7 @@ def _check_cwd_traversal(self, path_obj: Path | None = None) -> bool:
# If other resolution fails, treat as dangerous
return True

def _check_against_paths(
self, paths: list[str], path_obj: Path | None = None
) -> bool:
def _check_against_paths(self, paths: list[str], path_obj: Path | None = None) -> bool:
"""Check if a path matches any in the given list.

Args:
Expand Down Expand Up @@ -524,9 +520,7 @@ def _check_invalid_chars(self, path_str: str | None = None) -> bool:

return False

def __call__(
self, path: str | Path | None = None, raise_error: bool = False
) -> bool:
def __call__(self, path: str | Path | None = None, raise_error: bool = False) -> bool:
"""Check a path for danger, with optional path reload.

Note: Unlike the boolean context (which returns True for safe paths),
Expand Down Expand Up @@ -594,9 +588,7 @@ def __call__(
is_dangerous = True

if is_dangerous and raise_error:
raise DangerousPathError(
f"Path '{path}' points to a dangerous location"
)
raise DangerousPathError(f"Path '{path}' points to a dangerous location")

return is_dangerous
else:
Expand All @@ -605,9 +597,7 @@ def __call__(
is_dangerous = self._is_dangerous()

if is_dangerous and raise_error:
raise DangerousPathError(
f"Path '{self._path}' points to a dangerous location"
)
raise DangerousPathError(f"Path '{self._path}' points to a dangerous location")

return is_dangerous

Expand Down Expand Up @@ -966,6 +956,4 @@ def __new__(
cwd_only: bool = False,
) -> BasePathChecker:
"""Create a platform-specific PathChecker instance."""
return _create_path_checker(
path, raise_error, mode, system_ok, user_paths_ok, not_writeable, cwd_only
)
return _create_path_checker(path, raise_error, mode, system_ok, user_paths_ok, not_writeable, cwd_only)
6 changes: 1 addition & 5 deletions bad_path/platforms/windows/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,7 @@ def _check_invalid_chars(self, path_str: str | None = None) -> bool:
if char == ":":
# Check if colon is part of a drive letter (e.g., C:, D:)
# Valid pattern: single letter followed by colon at start of path
if (
len(path_str) >= 2
and path_str[1] == ":"
and path_str[0].isalpha()
):
if len(path_str) >= 2 and path_str[1] == ":" and path_str[0].isalpha():
# This is a valid drive letter if it's the only colon
if path_str.count(":") == 1:
continue # This is a valid drive letter colon
Expand Down
45 changes: 23 additions & 22 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,67 @@
import sys

# Add the package to the path
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))

# Import version from package
from bad_path import __version__

# Add sphinx-better-theme to the path
import better

html_theme_path = [better.better_theme_path]

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information

project = 'bad_path'
project_copyright = '2026, Gavin Burnell'
author = 'Gavin Burnell'
project = "bad_path"
project_copyright = "2026, Gavin Burnell"
author = "Gavin Burnell"
release = __version__
version = __version__

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.viewcode",
"sphinx.ext.intersphinx",
]

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'better'
html_static_path = ['_static']
html_theme = "better"
html_static_path = ["_static"]

# Theme options for sphinx-better-theme
html_theme_options = {
'rightsidebar': False,
'inlinecss': '',
'linktotheme': False,
"rightsidebar": False,
"inlinecss": "",
"linktotheme": False,
}

# Use organization logo
html_logo = '_static/StonerLogo2.png'
html_logo = "_static/StonerLogo2.png"

# -- Options for intersphinx extension ---------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration

intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
"python": ("https://docs.python.org/3", None),
}

# -- Options for autodoc extension -------------------------------------------

autodoc_default_options = {
'members': True,
'member-order': 'bysource',
'special-members': '__init__',
'undoc-members': True,
'exclude-members': '__weakref__'
"members": True,
"member-order": "bysource",
"special-members": "__init__",
"undoc-members": True,
"exclude-members": "__weakref__",
}
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ dev = [
"sphinx>=7.0",
"sphinx-better-theme>=0.1.5",
"ruff>=0.1.0",
"black>=24.0",
"pre-commit>=3.0",
]

[project.urls]
Expand Down Expand Up @@ -76,6 +78,10 @@ exclude_lines = [
"if TYPE_CHECKING:",
]

[tool.black]
line-length = 119
target-version = ['py310', 'py311', 'py312', 'py313']

[tool.ruff]
line-length = 100
target-version = "py310"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_invalid_characters.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def test_windows_invalid_chars():
if platform.system() != "Windows":
pytest.skip("Windows-specific test")

invalid_chars = ['<', '>', ':', '"', '|', '?', '*']
invalid_chars = ["<", ">", ":", '"', "|", "?", "*"]
for char in invalid_chars:
checker = PathChecker(f"C:\\tmp\\test{char}file.txt")
assert checker.has_invalid_chars is True, f"Character '{char}' should be invalid"
Expand Down
4 changes: 1 addition & 3 deletions tests/test_path_checker_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,7 @@ def test_invalid_chars_always_dangerous():
test_path = "/tmp/test\x00file.txt" # nosec B108

# Invalid chars should be dangerous even with all flags enabled
checker = PathChecker(
test_path, system_ok=True, user_paths_ok=True, not_writeable=True
)
checker = PathChecker(test_path, system_ok=True, user_paths_ok=True, not_writeable=True)
assert not checker # Still dangerous
assert checker.has_invalid_chars

Expand Down
4 changes: 1 addition & 3 deletions tests/test_path_checker_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ def test_mode_none_respects_individual_flags():
system_path = "/etc/passwd"

# mode=None with flags should work like before
checker = PathChecker(
system_path, mode=None, system_ok=True, not_writeable=True
)
checker = PathChecker(system_path, mode=None, system_ok=True, not_writeable=True)
assert checker # Safe with flags


Expand Down
Loading