diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c13a320 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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'] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5485daf..738152d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -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: @@ -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 @@ -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 diff --git a/bad_path/checker.py b/bad_path/checker.py index a133366..296fd50 100644 --- a/bad_path/checker.py +++ b/bad_path/checker.py @@ -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 @@ -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: @@ -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), @@ -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: @@ -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 @@ -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) diff --git a/bad_path/platforms/windows/checker.py b/bad_path/platforms/windows/checker.py index 271bec9..99acde4 100644 --- a/bad_path/platforms/windows/checker.py +++ b/bad_path/platforms/windows/checker.py @@ -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 diff --git a/docs/conf.py b/docs/conf.py index 44888b6..5ea9f19 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,21 +7,22 @@ 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__ @@ -29,44 +30,44 @@ # 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__", } diff --git a/pyproject.toml b/pyproject.toml index c6aa7f1..b86f280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] @@ -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" diff --git a/tests/test_invalid_characters.py b/tests/test_invalid_characters.py index 88ca05f..c459ba6 100644 --- a/tests/test_invalid_characters.py +++ b/tests/test_invalid_characters.py @@ -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" diff --git a/tests/test_path_checker_flags.py b/tests/test_path_checker_flags.py index a3752ae..dc2c050 100644 --- a/tests/test_path_checker_flags.py +++ b/tests/test_path_checker_flags.py @@ -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 diff --git a/tests/test_path_checker_mode.py b/tests/test_path_checker_mode.py index 61cd342..95789f4 100644 --- a/tests/test_path_checker_mode.py +++ b/tests/test_path_checker_mode.py @@ -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