diff --git a/.codex/instructions.md b/.codex/instructions.md new file mode 120000 index 00000000..4ff3f86e --- /dev/null +++ b/.codex/instructions.md @@ -0,0 +1 @@ +../.windsurfrules \ No newline at end of file diff --git a/.cursor/rules/avoid-debug-loops.mdc b/.cursor/rules/avoid-debug-loops.mdc new file mode 100644 index 00000000..8a241ec9 --- /dev/null +++ b/.cursor/rules/avoid-debug-loops.mdc @@ -0,0 +1,57 @@ +--- +description: When stuck in debugging loops, break the cycle by minimizing to an MVP, removing debugging cruft, and documenting the issue completely for a fresh approach +globs: *.py +alwaysApply: true +--- +# Avoid Debug Loops + +When debugging becomes circular and unproductive, follow these steps: + +## Detection +- You have made multiple unsuccessful attempts to fix the same issue +- You are adding increasingly complex code to address errors +- Each fix creates new errors in a cascading pattern +- You are uncertain about the root cause after 2-3 iterations + +## Action Plan + +1. **Pause and acknowledge the loop** + - Explicitly state that you are in a potential debug loop + - Review what approaches have been tried and failed + +2. **Minimize to MVP** + - Remove all debugging cruft and experimental code + - Revert to the simplest version that demonstrates the issue + - Focus on isolating the core problem without added complexity + +3. **Comprehensive Documentation** + - Provide a clear summary of the issue + - Include minimal but complete code examples that reproduce the problem + - Document exact error messages and unexpected behaviors + - Explain your current understanding of potential causes + +4. **Format for Portability** + - Present the problem in quadruple backticks for easy copying: + +```` +# Problem Summary +[Concise explanation of the issue] + +## Minimal Reproduction Code +```python +# Minimal code example that reproduces the issue +``` + +## Error/Unexpected Output +``` +[Exact error messages or unexpected output] +``` + +## Failed Approaches +[Brief summary of approaches already tried] + +## Suspected Cause +[Your current hypothesis about what might be causing the issue] +```` + +This format enables the user to easily copy the entire problem statement into a fresh conversation for a clean-slate approach. diff --git a/.cursor/rules/dev-loop.mdc b/.cursor/rules/dev-loop.mdc new file mode 100644 index 00000000..d60a5210 --- /dev/null +++ b/.cursor/rules/dev-loop.mdc @@ -0,0 +1,187 @@ +--- +description: QA every edit +globs: *.py +alwaysApply: true +--- + +# Development Process + +## Project Stack + +The project uses the following tools and technologies: + +- **uv** - Python package management and virtual environments +- **ruff** - Fast Python linter and formatter +- **py.test** - Testing framework + - **pytest-watcher** - Continuous test runner +- **mypy** - Static type checking +- **doctest** - Testing code examples in documentation + +## 1. Start with Formatting + +Format your code first: + +``` +uv run ruff format . +``` + +## 2. Run Tests + +Verify that your changes pass the tests: + +``` +uv run py.test +``` + +For continuous testing during development, use pytest-watcher: + +``` +# Watch all tests +uv run ptw . + +# Watch and run tests immediately, including doctests +uv run ptw . --now --doctest-modules + +# Watch specific files or directories +uv run ptw . --now --doctest-modules src/libtmux/_internal/ +``` + +## 3. Commit Initial Changes + +Make an atomic commit for your changes using conventional commits. +Use `@git-commits.mdc` for assistance with commit message standards. + +## 4. Run Linting and Type Checking + +Check and fix linting issues: + +``` +uv run ruff check . --fix --show-fixes +``` + +Check typings: + +``` +uv run mypy +``` + +## 5. Verify Tests Again + +Ensure tests still pass after linting and type fixes: + +``` +uv run py.test +``` + +## 6. Final Commit + +Make a final commit with any linting/typing fixes. +Use `@git-commits.mdc` for assistance with commit message standards. + +## Development Loop Guidelines + +If there are any failures at any step due to your edits, fix them before proceeding to the next step. + +## Python Code Standards + +### Docstring Guidelines + +For `src/**/*.py` files, follow these docstring guidelines: + +1. **Use reStructuredText format** for all docstrings. + ```python + """Short description of the function or class. + + Detailed description using reStructuredText format. + + Parameters + ---------- + param1 : type + Description of param1 + param2 : type + Description of param2 + + Returns + ------- + type + Description of return value + """ + ``` + +2. **Keep the main description on the first line** after the opening `"""`. + +3. **Use NumPy docstyle** for parameter and return value documentation. + +### Doctest Guidelines + +For doctests in `src/**/*.py` files: + +1. **Use narrative descriptions** for test sections rather than inline comments: + ```python + """Example function. + + Examples + -------- + Create an instance: + + >>> obj = ExampleClass() + + Verify a property: + + >>> obj.property + 'expected value' + """ + ``` + +2. **Move complex examples** to dedicated test files at `tests/examples//test_.py` if they require elaborate setup or multiple steps. + +3. **Utilize pytest fixtures** via `doctest_namespace` for more complex test scenarios: + ```python + """Example with fixture. + + Examples + -------- + >>> # doctest_namespace contains all pytest fixtures from conftest.py + >>> example_fixture = getfixture('example_fixture') + >>> example_fixture.method() + 'expected result' + """ + ``` + +4. **Keep doctests simple and focused** on demonstrating usage rather than comprehensive testing. + +5. **Add blank lines between test sections** for improved readability. + +6. **Test your doctests continuously** using pytest-watcher during development: + ``` + # Watch specific modules for doctest changes + uv run ptw . --now --doctest-modules src/path/to/module.py + ``` + +### Pytest Testing Guidelines + +1. **Use existing fixtures over mocks**: + - Use fixtures from conftest.py instead of `monkeypatch` and `MagicMock` when available + - For instance, if using libtmux, use provided fixtures: `server`, `session`, `window`, and `pane` + - Document in test docstrings why standard fixtures weren't used for exceptional cases + +2. **Preferred pytest patterns**: + - Use `tmp_path` (pathlib.Path) fixture over Python's `tempfile` + - Use `monkeypatch` fixture over `unittest.mock` + +### Import Guidelines + +1. **Prefer namespace imports**: + - Import modules and access attributes through the namespace instead of importing specific symbols + - Example: Use `import enum` and access `enum.Enum` instead of `from enum import Enum` + - This applies to standard library modules like `pathlib`, `os`, and similar cases + +2. **Standard aliases**: + - For `typing` module, use `import typing as t` + - Access typing elements via the namespace: `t.NamedTuple`, `t.TypedDict`, etc. + - Note primitive types like unions can be done via `|` pipes and primitive types like list and dict can be done via `list` and `dict` directly. + +3. **Benefits of namespace imports**: + - Improves code readability by making the source of symbols clear + - Reduces potential naming conflicts + - Makes import statements more maintainable diff --git a/.cursor/rules/git-commits.mdc b/.cursor/rules/git-commits.mdc new file mode 100644 index 00000000..f9c0980d --- /dev/null +++ b/.cursor/rules/git-commits.mdc @@ -0,0 +1,95 @@ +--- +description: git-commits: Git commit message standards and AI assistance +globs: git-commits: Git commit message standards and AI assistance | *.git/* .gitignore .github/* CHANGELOG.md CHANGES.md +alwaysApply: true +--- +# Optimized Git Commit Standards + +## Commit Message Format +``` +Component/File(commit-type[Subcomponent/method]): Concise description + +why: Explanation of necessity or impact. +what: +- Specific technical changes made +- Focused on a single topic + +refs: #issue-number, breaking changes, or relevant links +``` + +## Component Patterns +### General Code Changes +``` +Component/File(feat[method]): Add feature +Component/File(fix[method]): Fix bug +Component/File(refactor[method]): Code restructure +``` + +### Packages and Dependencies +| Language | Standard Packages | Dev Packages | Extras / Sub-packages | +|------------|------------------------------------|-------------------------------|-----------------------------------------------| +| General | `lang(deps):` | `lang(deps[dev]):` | | +| Python | `py(deps):` | `py(deps[dev]):` | `py(deps[extra]):` | +| JavaScript | `js(deps):` | `js(deps[dev]):` | `js(deps[subpackage]):`, `js(deps[dev{subpackage}]):` | + +#### Examples +- `py(deps[dev]): Update pytest to v8.1` +- `js(deps[ui-components]): Upgrade Button component package` +- `js(deps[dev{linting}]): Add ESLint plugin` + +### Documentation Changes +Prefix with `docs:` +``` +docs(Component/File[Subcomponent/method]): Update API usage guide +``` + +### Test Changes +Prefix with `tests:` +``` +tests(Component/File[Subcomponent/method]): Add edge case tests +``` + +## Commit Types Summary +- **feat**: New features or enhancements +- **fix**: Bug fixes +- **refactor**: Code restructuring without functional change +- **docs**: Documentation updates +- **chore**: Maintenance (dependencies, tooling, config) +- **test**: Test-related updates +- **style**: Code style and formatting + +## General Guidelines +- Subject line: Maximum 50 characters +- Body lines: Maximum 72 characters +- Use imperative mood (e.g., "Add", "Fix", not "Added", "Fixed") +- Limit to one topic per commit +- Separate subject from body with a blank line +- Mark breaking changes clearly: `BREAKING:` +- Use `See also:` to provide external references + +## AI Assistance Workflow in Cursor +- Stage changes with `git add` +- Use `@commit` to generate initial commit message +- Review and refine generated message +- Ensure adherence to these standards + +## Good Commit Example +``` +Pane(feat[capture_pane]): Add screenshot capture support + +why: Provide visual debugging capability +what: +- Implement capturePane method with image export +- Integrate with existing Pane component logic +- Document usage in Pane README + +refs: #485 +See also: https://example.com/docs/pane-capture +``` + +## Bad Commit Example +``` +fixed stuff and improved some functions +``` + +These guidelines ensure clear, consistent commit histories, facilitating easier code review and maintenance. \ No newline at end of file diff --git a/.cursor/rules/pytest-best-practices.mdc b/.cursor/rules/pytest-best-practices.mdc new file mode 100644 index 00000000..35af3abf --- /dev/null +++ b/.cursor/rules/pytest-best-practices.mdc @@ -0,0 +1,145 @@ +--- +description: +globs: tests/**/*.py +alwaysApply: false +--- +# Ground Rules for Writing vcspull Tests + +## 1. Study First, Be Homogenous +- **Analyze Existing Tests:** Before writing new tests, study `tests/test_sync.py`, `tests/test_config.py`, and other relevant test files to understand existing patterns, fixture usage, and assertion styles. +- **Maintain Consistency:** Strive for homogeneity in test structure, naming conventions, and overall style. +- **Project Conventions:** Adhere to project-specific conventions like using `typing as t` and `pathlib.Path` for all path manipulations. + +## 2. Fixture Prioritization and Usage +- **`libvcs` Fixtures First:** For any VCS-related operations (creating repos, committing, checking status), **always** prioritize using fixtures from `libvcs.pytest_plugin` (available via `vcspull/conftest.py`). Examples: + - `create_git_remote_repo`, `create_svn_remote_repo`, `create_hg_remote_repo` (and their `mercurial`/`subversion` counterparts if using those directly) for setting up test repositories. + - `git_repo`, `svn_repo`, `hg_repo` for pre-configured `Sync` objects. + - `git_commit_envvars` for environment variables needed for git commits. +- **Pytest Built-in Fixtures:** Utilize standard `pytest` fixtures like `tmp_path` for temporary files and directories. +- **Custom Project Fixtures:** + - For common non-VCS setup (e.g., mocked home/CWD, config file setup), use or create well-defined fixtures. + - Place shared fixtures in `vcspull/conftest.py` or `vcspull/tests/conftest.py`. Module-specific fixtures can reside in the test file itself. + - Example: `home_path`, `cwd_path` (refactored to use `monkeypatch`), `setup_teardown_test_config_dir`. +- **`autouse=True`:** Use sparingly, only for fixtures that genuinely apply to *all* tests within their scope. + +## 3. Mocking Strategy: `monkeypatch` vs. `mocker` +- **`monkeypatch` (pytest built-in):** + - **Environment & Globals:** Use for modifying global settings, environment variables (`monkeypatch.setenv()`, `monkeypatch.delenv()`), the current working directory (`monkeypatch.chdir()`), or `sys.path`. + - **Patching Attributes/Builtins:** Use `monkeypatch.setattr()` to modify attributes of classes/objects (e.g., `pathlib.Path.home`) or to replace functions/methods in external libraries or Python builtins. When needing to control the home directory, prefer using established project fixtures like `user_path`, `home_path`, or `config_path`. These fixtures are responsible for correctly mocking `pathlib.Path.home()` internally, typically using `monkeypatch.setattr()`. Avoid direct `monkeypatch.setattr(pathlib.Path, "home", ...)` in individual tests if a suitable project fixture exists. + - **Dictionary Items:** Use `monkeypatch.setitem()` and `monkeypatch.delitem()` for modifying dictionaries. + - Refer to [Pytest Monkeypatch Documentation](mdc:.dot-config/https:/docs.pytest.org/en/stable/how-to/monkeypatch.html). +- **`mocker` (from `pytest-mock`):** + - **Application Code:** Primarily use for patching functions, methods, or objects *within the `vcspull` application code itself* (e.g., `mocker.patch('vcspull.cli.add.some_function')`). + - **Assertions:** Use `mocker` when you need to assert how a mock was called, its return values, or to simulate side effects for your application's internal logic. +- **Clarity in Mocking (CRITICAL):** + - For **every** use of `mocker.patch()`, `mocker.patch.object()`, `monkeypatch.setattr()`, `monkeypatch.setenv()`, etc., include comments explaining: + - **`# WHAT:`**: What specific function, method, attribute, or environment variable is being simulated or altered. + - **`# WHY:`**: The reason for the mock – what behavior is being controlled or isolated for the test's purpose. + +## 4. Test Structure and Assertions +- **Atomic Tests:** Each test function should verify a single, specific piece of functionality or scenario. +- **Clear Naming:** Test functions and fixtures should have descriptive names (e.g., `test_add_repo_new_config_cwd`). +- **Docstrings:** Test functions should have a concise docstring explaining what is being tested. +- **Plain Assertions:** Use standard `assert` statements for verifications. +- **Logging:** Use the `caplog` fixture to assert specific log messages when testing command output or internal logging. +- **Error Handling:** Explicitly test for expected exceptions using `pytest.raises()`. + +### Parameterized Test Structure +For tests involving multiple scenarios managed by `@pytest.mark.parametrize`, use `typing.NamedTuple` to define the structure of each test case. This promotes readability and consistency. +- Include a `test_id: str` field in the `NamedTuple` for clear test identification in pytest output. +- Define a list of these `NamedTuple` instances for your test scenarios. +- Use `pytest.mark.parametrize` with `ids=lambda tc: tc.test_id` (or similar) for descriptive test names. + +```python +# Example of Parameterized Test Structure +import typing as t +import pytest + +class MyTestScenario(t.NamedTuple): + test_id: str + input_arg: str + expected_output: str + # ... other relevant parameters + +TEST_SCENARIOS: list[MyTestScenario] = [ + MyTestScenario(test_id="case_alpha", input_arg="foo", expected_output="bar"), + MyTestScenario(test_id="case_beta", input_arg="baz", expected_output="qux"), +] + +@pytest.mark.parametrize( + MyTestScenario._fields, + TEST_SCENARIOS, + ids=[tc.test_id for tc in TEST_SCENARIOS] # Or ids=lambda tc: tc.test_id +) +def test_my_feature( + input_arg: str, expected_output: str, # Corresponds to NamedTuple fields (test_id usually not passed) + # ... other fixtures ... + test_id: str, # if you need test_id inside the test, otherwise omit from signature +) -> None: + # ... test logic using input_arg and asserting against expected_output ... + actual_output = f"processed_{input_arg}" # Replace with actual function call + assert actual_output == expected_output + # Note: test_id is automatically unpacked by parametrize if present in NamedTuple fields + # and also passed as an argument if included in the test function signature. +``` + +### Asserting CLI Output +When testing CLI commands using `capsys` (for stdout/stderr) or `caplog` (for log messages), define expected output clearly. A common pattern is to check for the presence (or absence) of a list of substring "needles" within the captured output. + +```python +# Example of Asserting CLI Output +# (within a test function that uses capsys or caplog) + +# Assuming your NamedTuple for parameters includes: +# expected_in_out: t.Optional[t.Union[str, list[str]]] = None +# expected_not_in_out: t.Optional[t.Union[str, list[str]]] = None + +# Example: Capturing stdout +# captured_stdout = capsys.readouterr().out +# output_to_check = captured_stdout + +# Example: Capturing log messages +# output_to_check = \"\\n\".join(rec.message for rec in caplog.records) + + +# Generic checking logic: +# if expected_in_out is not None: +# needles = [expected_in_out] if isinstance(expected_in_out, str) else expected_in_out +# for needle in needles: +# assert needle in output_to_check, f"Expected '{needle}' in output" + +# if expected_not_in_out is not None: +# needles = [expected_not_in_out] if isinstance(expected_not_in_out, str) else expected_not_in_out +# for needle in needles: +# assert needle not in output_to_check, f"Did not expect '{needle}' in output" +``` + +## 5. Code Coverage and Quality +- **100% Coverage:** Aim for 100% test coverage for all new or modified code in `cli/add.py` and `cli/add_from_fs.py` (and any other modules). +- **Test All Paths:** Ensure tests cover success cases, failure cases, edge conditions, and all logical branches within the code. +- **Development Workflow:** Adhere to the project's quality assurance process: + ``` + uv run ruff check . --fix + uv run ruff format . + uv run mypy + uv run py.test --cov -v + ``` + (Run these commands locally to verify changes). + +## 6. `vcspull`-Specific Considerations +- **Configuration Files:** + - When testing config loading, mock `find_home_config_files` appropriately. + - **Always** use the project's helper functions (e.g., `vcspull.tests.helpers.write_config` or the higher-level `vcspull.tests.helpers.save_config_yaml`) to create temporary `.vcspull.yaml` files within your tests (typically in `tmp_path` or `config_path`). Avoid direct `yaml.dump` and `file.write_text` for config file creation to maintain consistency and reduce boilerplate. +- **Path Expansion:** Be mindful of `expand_dir`. If testing logic that depends on its behavior, provide controlled mocks for it or ensure `home_path` / `cwd_path` fixtures correctly influence its resolution. +- **Avoid Manual VCS Subprocesses:** + - **Do not** use `subprocess.run(["git", ...])` or similar direct VCS command calls for setting up repository states in tests if a `libvcs` fixture or library function can achieve the same result. + - The only exception is when testing a function that *itself* directly uses `subprocess` (e.g., `get_git_origin_url`). + - Refactor tests like `test_add_from_fs_integration_with_libvcs` to use `create_git_remote_repo` from `libvcs` instead of manual `git init` calls via `subprocess`. + +### Filesystem State Management and `shutil` +- **`tmp_path` is Primary:** Rely on `pytest` fixtures like `tmp_path` (and derived fixtures such as `user_path`, `config_path`) for creating and managing temporary files and directories needed by tests. Pytest automatically handles the cleanup of these resources. +- **Avoid Manual Cleanup of `tmp_path` Resources:** Manual cleanup of files/directories created within `tmp_path` using `shutil` (e.g., `shutil.rmtree()`) should generally be unnecessary, as pytest's fixture management handles this. +- **`shutil` for Pre-conditions:** However, `shutil` operations (or standard library functions like `os.remove`, `pathlib.Path.unlink`, `pathlib.Path.mkdir`) can be legitimately used *during the setup phase of a test (before the code under test is executed)*. This is appropriate when you need to establish specific filesystem pre-conditions *within* the `tmp_path` environment that are crucial for the test scenario. + - **Example:** Ensuring a directory does *not* exist before testing an operation that is expected to create it, or ensuring a specific file *does* exist. The usage of `if my_git_repo.is_dir(): shutil.rmtree(my_git_repo)` in `tests/test_cli.py` (within `test_sync_broken`) is an example of setting such a pre-condition: it ensures that the target directory for a repository is removed before `vcspull sync` is called, allowing tests to consistently observe cloning behavior. + +By following these rules, we can ensure that new tests are robust, maintainable, consistent with the existing test suite, and effectively leverage the capabilities of `pytest` and `libvcs`. \ No newline at end of file diff --git a/.cursor/rules/vcspull-pytest.mdc b/.cursor/rules/vcspull-pytest.mdc new file mode 100644 index 00000000..033b504f --- /dev/null +++ b/.cursor/rules/vcspull-pytest.mdc @@ -0,0 +1,86 @@ +--- +description: +globs: tests/**/test_*.py +alwaysApply: true +--- + +# VCSPull Pytest Integration with libvcs + +When writing tests for vcspull, leverage libvcs's pytest plugin to efficiently create and manage VCS repositories during testing. + +## Available Fixtures from libvcs + +libvcs provides a complete set of fixtures that automatically handle the creation and cleanup of VCS repositories: + +### Core Repository Creation Fixtures + +- `create_git_remote_repo`: Factory fixture that creates a local Git repository +- `create_svn_remote_repo`: Factory fixture that creates a local SVN repository +- `create_hg_remote_repo`: Factory fixture that creates a local Mercurial repository + +### Pre-configured Repository Fixtures + +- `git_repo`: Pre-made Git repository clone (GitSync instance) +- `svn_repo`: Pre-made SVN repository checkout (SvnSync instance) +- `hg_repo`: Pre-made Mercurial repository clone (HgSync instance) + +### Environment & Configuration Fixtures + +- `set_home`: Sets a temporary home directory +- `gitconfig`: Git configuration for test repositories +- `hgconfig`: Mercurial configuration for test repositories +- `git_commit_envvars`: Environment variables for Git commits + +## Usage Examples + +### Basic Repository Creation + +```python +def test_vcspull_with_git(create_git_remote_repo): + # Create a test git repository on-the-fly + repo_path = create_git_remote_repo() + + # repo_path is now a pathlib.Path pointing to a clean git repo + # Use this repository in your vcspull tests +``` + +### Using Pre-configured Repositories + +```python +def test_vcspull_sync(git_repo): + # git_repo is already a GitSync instance with a clean repository + # Use it directly in your tests + + # The repository will be automatically cleaned up after the test +``` + +### Custom Repository Setup + +```python +def test_custom_repo_state( + create_git_remote_repo, + git_commit_envvars +): + # Create a repo with custom initialization + repo_path = create_git_remote_repo() + + # Modify the repository as needed with the correct environment + import subprocess + subprocess.run( + ["git", "commit", "--allow-empty", "-m", "Custom commit"], + cwd=repo_path, + env=git_commit_envvars + ) +``` + +## Benefits + +- **Fast tests**: Repositories are created efficiently and cached appropriately +- **Clean environment**: Each test gets fresh repositories without interference +- **Reduced boilerplate**: No need to manually create/clean up repositories +- **Realistic testing**: Test against actual VCS operations +- **Compatible with pytest-xdist**: Works correctly with parallel test execution + +For detailed documentation on all available fixtures, visit: +- https://libvcs.git-pull.com/pytest-plugin.html +- https://github.com/vcs-python/libvcs/blob/master/src/libvcs/pytest_plugin.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..d202a332 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 5da69bd9..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - schedule: - - cron: '16 5 * * 2' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: "python" - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7e486789..58b9c47f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10"] + python-version: ['3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Filter changed file paths to outputs - uses: dorny/paths-filter@v2.7.0 + uses: dorny/paths-filter@v3.0.2 id: changes with: filters: | @@ -28,37 +28,36 @@ jobs: python_files: - 'vcspull/**' - pyproject.toml - - poetry.lock + - uv.lock - name: Should publish if: steps.changes.outputs.docs == 'true' || steps.changes.outputs.root_docs == 'true' || steps.changes.outputs.python_files == 'true' run: echo "PUBLISH=$(echo true)" >> $GITHUB_ENV - - name: Install poetry + - name: Install uv + uses: astral-sh/setup-uv@v5 if: env.PUBLISH == 'true' - run: pipx install "poetry==1.2.1" + with: + enable-cache: true - name: Set up Python ${{ matrix.python-version }} if: env.PUBLISH == 'true' - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'poetry' + run: uv python install ${{ matrix.python-version }} - - name: Install dependencies [w/ docs] + - name: Install dependencies if: env.PUBLISH == 'true' - run: poetry install --extras "docs lint" + run: uv sync --all-extras --dev - name: Print python versions if: env.PUBLISH == 'true' run: | python -V - poetry run python -V + uv run python -V - name: Build documentation if: env.PUBLISH == 'true' run: | - pushd docs; make SPHINXBUILD='poetry run sphinx-build' html; popd + pushd docs; make SPHINXBUILD='uv run sphinx-build' html; popd - name: Push documentation to S3 if: env.PUBLISH == 'true' @@ -69,8 +68,8 @@ jobs: AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: "us-west-1" # optional: defaults to us-east-1 - SOURCE_DIR: "docs/_build/html" # optional: defaults to entire repository + AWS_REGION: 'us-west-1' # optional: defaults to us-east-1 + SOURCE_DIR: 'docs/_build/html' # optional: defaults to entire repository - name: Purge cache on Cloudflare if: env.PUBLISH == 'true' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8b9b81ba..ca88dee3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,41 +9,43 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10"] + python-version: ['3.9', '3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install "poetry==1.2.1" + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'poetry' + run: uv python install ${{ matrix.python-version }} - name: Install dependencies - run: poetry install -E "docs test coverage lint format" + run: uv sync --all-extras --dev - name: Print python versions run: | python -V - poetry run python -V + uv run python -V - - name: Lint with flake8 - run: poetry run flake8 + - name: Lint with ruff check + run: uv run ruff check . + + - name: Format with ruff format + run: uv run ruff format . --check - name: Lint with mypy - run: poetry run mypy . + run: uv run mypy . - name: Test with pytest - run: poetry run py.test --cov=./ --cov-append --cov-report=xml + run: uv run py.test --cov=./ --cov-append --cov-report=xml env: COV_CORE_SOURCE: . COV_CORE_CONFIG: pyproject.toml COV_CORE_DATAFILE: .coverage.eager - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -54,23 +56,25 @@ jobs: strategy: matrix: - python-version: ["3.10"] + python-version: ['3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Install poetry - run: pipx install "poetry==1.2.1" + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'poetry' + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev - name: Build package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - run: poetry build + run: uv build - name: Publish package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') diff --git a/.gitignore b/.gitignore index f20f3ff7..d0f22f7e 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,5 @@ pip-wheel-metadata/ # Monkeytype monkeytype.sqlite3 + +**/.claude/settings.local.json diff --git a/.python-version b/.python-version index 5ca284fc..4eba2a62 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10.7 3.9.13 +3.13.0 diff --git a/.tmuxp.yaml b/.tmuxp.yaml index b406f94f..2abfeef1 100644 --- a/.tmuxp.yaml +++ b/.tmuxp.yaml @@ -1,21 +1,22 @@ session_name: vcspull start_directory: ./ # load session relative to config location (project root). shell_command_before: -- '[ -f .venv/bin/activate ] && source .venv/bin/activate && reset' +- uv virtualenv --quiet > /dev/null 2>&1 && clear windows: - window_name: vcspull focus: True layout: main-horizontal options: - main-pane-height: 35 + main-pane-height: 67% panes: - focus: true - pane + - make watch_mypy - make watch_test - window_name: docs layout: main-horizontal options: - main-pane-height: 35 + main-pane-height: 67% start_directory: docs/ panes: - focus: true diff --git a/.tool-versions b/.tool-versions index b4576c08..15946ec4 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -poetry 1.2.1 -python 3.10.7 3.9.13 +uv 0.7.16 +python 3.13.5 3.12.11 3.11.13 3.10.18 3.9.23 diff --git a/.vim/coc-settings.json b/.vim/coc-settings.json new file mode 100644 index 00000000..d542a726 --- /dev/null +++ b/.vim/coc-settings.json @@ -0,0 +1,19 @@ +{ + "[markdown][python]": { + "coc.preferences.formatOnSave": true + }, + "python.analysis.autoSearchPaths": true, + "python.analysis.typeCheckingMode": "basic", + "python.analysis.useLibraryCodeForTypes": true, + "python.formatting.provider": "ruff", + "python.linting.ruffEnabled": true, + "python.linting.mypyEnabled": true, + "python.linting.flake8Enabled": false, + "python.linting.pyflakesEnabled": false, + "python.linting.pycodestyleEnabled": false, + "python.linting.banditEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": false, + "pyright.organizeimports.provider": "ruff", + "pyright.testing.provider": "pytest", +} diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 00000000..3ccb1bf2 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,162 @@ +# Python Project Rules + + +- uv - Python package management and virtual environments +- ruff - Fast Python linter and formatter +- py.test - Testing framework + - pytest-watcher - Continuous test runner +- mypy - Static type checking +- doctest - Testing code examples in documentation + + + +- Use a consistent coding style throughout the project +- Format code with ruff before committing +- Run linting and type checking before finalizing changes +- Verify tests pass after each significant change + + + +- Use reStructuredText format for all docstrings in src/**/*.py files +- Keep the main description on the first line after the opening `"""` +- Use NumPy docstyle for parameter and return value documentation +- Format docstrings as follows: + ```python + """Short description of the function or class. + + Detailed description using reStructuredText format. + + Parameters + ---------- + param1 : type + Description of param1 + param2 : type + Description of param2 + + Returns + ------- + type + Description of return value + """ + ``` + + + +- Use narrative descriptions for test sections rather than inline comments +- Format doctests as follows: + ```python + """ + Examples + -------- + Create an instance: + + >>> obj = ExampleClass() + + Verify a property: + + >>> obj.property + 'expected value' + """ + ``` +- Add blank lines between test sections for improved readability +- Keep doctests simple and focused on demonstrating usage +- Move complex examples to dedicated test files at tests/examples//test_.py +- Utilize pytest fixtures via doctest_namespace for complex scenarios + + + +- Run tests with `uv run py.test` before committing changes +- Use pytest-watcher for continuous testing: `uv run ptw . --now --doctest-modules` +- Fix any test failures before proceeding with additional changes + + + +- Make atomic commits with conventional commit messages +- Start with an initial commit of functional changes +- Follow with separate commits for formatting, linting, and type checking fixes + + + +- Use the following commit message format: + ``` + Component/File(commit-type[Subcomponent/method]): Concise description + + why: Explanation of necessity or impact. + what: + - Specific technical changes made + - Focused on a single topic + + refs: #issue-number, breaking changes, or relevant links + ``` + +- Common commit types: + - **feat**: New features or enhancements + - **fix**: Bug fixes + - **refactor**: Code restructuring without functional change + - **docs**: Documentation updates + - **chore**: Maintenance (dependencies, tooling, config) + - **test**: Test-related updates + - **style**: Code style and formatting + +- Prefix Python package changes with: + - `py(deps):` for standard packages + - `py(deps[dev]):` for development packages + - `py(deps[extra]):` for extras/sub-packages + +- General guidelines: + - Subject line: Maximum 50 characters + - Body lines: Maximum 72 characters + - Use imperative mood (e.g., "Add", "Fix", not "Added", "Fixed") + - Limit to one topic per commit + - Separate subject from body with a blank line + - Mark breaking changes clearly: `BREAKING:` + + + +- Use fixtures from conftest.py instead of monkeypatch and MagicMock when available +- For instance, if using libtmux, use provided fixtures: server, session, window, and pane +- Document in test docstrings why standard fixtures weren't used for exceptional cases +- Use tmp_path (pathlib.Path) fixture over Python's tempfile +- Use monkeypatch fixture over unittest.mock + + + +- Prefer namespace imports over importing specific symbols +- Import modules and access attributes through the namespace: + - Use `import enum` and access `enum.Enum` instead of `from enum import Enum` + - This applies to standard library modules like pathlib, os, and similar cases +- For typing, use `import typing as t` and access via the namespace: + - Access typing elements as `t.NamedTuple`, `t.TypedDict`, etc. + - Note primitive types like unions can be done via `|` pipes + - Primitive types like list and dict can be done via `list` and `dict` directly +- Benefits of namespace imports: + - Improves code readability by making the source of symbols clear + - Reduces potential naming conflicts + - Makes import statements more maintainable + + + +- Use libvcs pytest fixtures for all repository-related tests in vcspull: + - Create temporary repositories efficiently with factory fixtures + - Benefit from automatic cleanup when tests finish + - Utilize proper environment variables and configurations + - Test against real VCS operations without mocking + +- Basic repository testing pattern: + ```python + def test_repository_operation(create_git_remote_repo): + # Create a test repository + repo_path = create_git_remote_repo() + + # Test vcspull functionality with the repository + # ... + ``` + +- For more complex scenarios, use the pre-configured repository instances: + ```python + def test_sync_operations(git_repo): + # git_repo is already a GitSync instance + # Test vcspull sync operations + # ... + ``` + diff --git a/CHANGES b/CHANGES index ece75948..e2821a0d 100644 --- a/CHANGES +++ b/CHANGES @@ -15,12 +15,409 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force // Usage: vcspull@next sync [config] ``` -## vcspull v1.18.x (unreleased) +## vcspull v1.35.x (unreleased) - _Notes on upcoming releases will be added here_ +### Development + +- libvcs 0.35.1 -> 0.36.0 (#467) + + Improved Git URL detection + +## vcspull v1.34.1 (2025-06-21) + +### Development + +- libvcs 0.35.0 -> 0.35.1 (#466) + + Fixes issue with console streaming output, e.g. `git clone` progress. + +## vcspull v1.34.0 (2025-02-22) + +### Development + +#### libvcs: 0.34.0 -> 0.35.0 (#461) + +Internally, this libvcs will use `text=True` in subprocess, and deal only with +unicode. Before this change, handling bytestrings was supported. + +#### chore: Implement PEP 563 deferred annotation resolution (#459) + +- Add `from __future__ import annotations` to defer annotation resolution and reduce unnecessary runtime computations during type checking. +- Enable Ruff checks for PEP-compliant annotations: + - [non-pep585-annotation (UP006)](https://docs.astral.sh/ruff/rules/non-pep585-annotation/) + - [non-pep604-annotation (UP007)](https://docs.astral.sh/ruff/rules/non-pep604-annotation/) + +For more details on PEP 563, see: https://peps.python.org/pep-0563/ + +## vcspull v1.33.0 (2024-11-23) + +_Maintenance only, no bug fixes, or new features_ + +### Breaking changes + +#### Project and package management: poetry to uv (#457) + +[uv] is the new package and project manager for the project, replacing Poetry. + +[uv]: https://github.com/astral-sh/uv + +#### Build system: poetry to hatchling (#457) + +[Build system] moved from [poetry] to [hatchling]. + +[Build system]: https://packaging.python.org/en/latest/tutorials/packaging-projects/#choosing-a-build-backend +[poetry]: https://github.com/python-poetry/poetry +[hatchling]: https://hatch.pypa.io/latest/ + +#### libvcs 0.33.0 -> 0.34.0 (#456) + +First libvcs release built with `uv` package and project manager. No changes to libvcs API itself. + +## vcspull v1.32.0 (2024-10-13) + +_Maintenance only, no bug fixes or new features_ + +### New features + +#### Python 3.13 support + +Added Python 3.13 to package trove classifiers and CI tests. + +#### libvcs 0.30.1 -> 0.33.0 (#453) + +Faster, cached pytest fixtures. Upstream tests are 50% faster. + +- https://github.com/vcs-python/libvcs/blob/v0.32.1/CHANGES#libvcs-0310-2024-10-12 +- https://libvcs.git-pull.com/history.html#libvcs-0-32-1-2024-10-12 + +#### Faster tests fixtures (#453) + +libvcs's included test fixtures beyond v0.31.0+ are 33%+ faster. + +## vcspull v1.31.0 (2024-06-18) + +### Breaking changes + +- libvcs: 0.29.0 -> 0.30.1 (#449, #450) + + [AWS CodeCommit] support. Example: + + ```yaml + ~/study/aws-codecommit: + test: + repo: "codecommit::us-east-1://test" + ``` + +[AWS CodeCommit]: https://docs.aws.amazon.com/codecommit/ + +### Documentation + +- Automatically linkify links that were previously only text. + +### Development + +- poetry: 1.8.1 -> 1.8.2 + + See also: https://github.com/python-poetry/poetry/blob/1.8.2/CHANGELOG.md + +- Code quality: Use f-strings in more places (#445) + + via [ruff 0.4.2](https://github.com/astral-sh/ruff/blob/v0.4.2/CHANGELOG.md). + +## vcspull v1.30.0 (2024-03-24) + +_Maintenance only, no bug fixes or new features_ + +### Development + +- Aggressive automated lint fixes via `ruff` (#442) + + via ruff v0.3.4, all automated lint fixes, including unsafe and previews were applied: + + ```sh + ruff check --select ALL . --fix --unsafe-fixes --preview --show-fixes; ruff format . + ``` + + Branches were treated with: + + ```sh + git rebase \ + --strategy-option=theirs \ + --exec 'poetry run ruff check --select ALL . --fix --unsafe-fixes --preview --show-fixes; poetry run ruff format .; git add src tests; git commit --amend --no-edit' \ + origin/master + ``` + +## vcspull v1.29.0 (2024-03-24) + +_Maintenance only, no bug fixes or new features_ + +### Breaking changes + +- libvcs: 0.28.2 -> 0.29.0 (#443) + + Internal refactorings and maintenance. + +### Development + +- poetry: 1.7.1 -> 1.8.1 + + See also: https://github.com/python-poetry/poetry/blob/1.8.1/CHANGELOG.md + +- ruff 0.2.2 -> 0.3.0 (#441) + + Related formattings. Update CI to use `ruff check .` instead of `ruff .`. + + See also: https://github.com/astral-sh/ruff/blob/v0.3.0/CHANGELOG.md + +## vcspull v1.28.1 (2024-02-17) + +### Fixes + +- libvcs: 0.28.0 -> 0.28.1 (#440) + + Fixes `'--max-count': not an integer` issue when syncing git repos. + +### Testing + +- CI: Bump actions for Node 20 (#439) + +## vcspull v1.28.0 (2024-02-07) + +_Maintenance only, no bug fixes or new features_ + +### Breaking changes + +- libvcs: 0.27.0 -> 0.28.0 (#437) + + QueryList generic typing improvements. + +## vcspull v1.27.0 (2024-02-07) + +_Maintenance only, no bug fixes, or new features_ + +### Breaking changes + +- libvcs: v0.26.0 -> v0.27.0 (#435) + + Renamings of `dir` to `path`. + +- Fix shadowing of python builtins + + - `dir` -> `path` (#435) + +### Development + +- Strengthen linting (#436) + + - Add flake8-commas (COM) + + - https://docs.astral.sh/ruff/rules/#flake8-commas-com + - https://pypi.org/project/flake8-commas/ + + - Add flake8-builtins (A) + + - https://docs.astral.sh/ruff/rules/#flake8-builtins-a + - https://pypi.org/project/flake8-builtins/ + + - Add flake8-errmsg (EM) + + - https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + - https://pypi.org/project/flake8-errmsg/ + +### Documentation + +- Refactor API docs to split across multiple pages (#431) +- Remove unused reStructuredText section headers from some modules (#431) + +## vcspull v1.26.0 (2023-12-16) + +_Maintenance only, no bug fixes, or new features_ + +### CI + +- ci: Add pydocstyle rule to ruff (#428) + +### Documentation + +- Add docstrings to functions, methods, classes, and packages (#428) + +## vcspull v1.25.0 (2023-11-26) + +### Packaging + +- libvcs 0.25.1 -> 0.26.0: Minor bug fixes, docstring updates + +### CI + +- Move CodeQL from advanced configuration file to GitHub's default + +## vcspull v1.24.1 (2023-11-24) + +### Packaging + +- libvcs 0.25.0 -> 0.25.1 (maintenance release) + +## vcspull v1.24.0 (2023-11-19) + +_Maintenance only, no bug fixes, or new features_ + +### Packaging + +- libvcs 0.24.0 -> 0.25.0 (maintenance release) +- Add Python 3.12 to trove classifiers +- Poetry: 1.6.x -> 1.7.0 + + See also: https://github.com/python-poetry/poetry/blob/1.7.0/CHANGELOG.md + +- Packaging (poetry): Fix development dependencies + + Per [Poetry's docs on managing dependencies] and `poetry check`, we had it wrong: Instead of using extras, we should create these: + + ```toml + [tool.poetry.group.group-name.dependencies] + dev-dependency = "1.0.0" + ``` + + Which we now do. + + [Poetry's docs on managing dependencies]: https://python-poetry.org/docs/master/managing-dependencies/ + +### Development + +- Move formatting from `black` to [`ruff format`] (#427) + + This retains the same formatting style of `black` while eliminating a + dev dependency by using our existing rust-based `ruff` linter. + + [`ruff format`]: https://docs.astral.sh/ruff/formatter/ + +- CI: Update action packages to fix warnings + + - [dorny/paths-filter]: 2.7.0 -> 2.11.1 + + [dorny/paths-filter]: https://github.com/dorny/paths-filter + +## vcspull v1.23.0 (2023-10-22) + +### Breaking changes + +- libvcs: Bumped from 0.22.2 -> 0.24.0 (#419) + +### Bug fixes + +- Git Remote URLs: Fix bug that would cause git remotes with `@` to be chopped off after the + protocol (#419, fixes #425) + +### Development + +- Refactor of two testsuites to used `NamedTuple` parametrization (#423): + + - test_config_variations + - test_updating_remote + +## vcspull v1.22.0 (2023-09-02) + +_Maintenance only, no bug fixes, or new features_ + +### Development + +- Code quality improved via [ruff] rules (#417) + + This includes fixes made by hand, and with ruff's automated fixes. With + the expanded set of linting rules (which include import sorting) - ruff runs are + still instant when checking the whole codebase. + +## vcspull v1.21.1 (2023-05-28) + +_Maintenance only, no bug fixes, or new features_ + +### Development + +- Add back `black` for formatting + + This is still necessary to accompany `ruff`, until it replaces black. + +## vcspull v1.21.0 (2023-05-27) + +_Maintenance only, no bug fixes or features_ + +### Internal improvements + +- Move formatting, import sorting, and linting to [ruff]. + + This rust-based checker has dramatically improved performance. Linting and + formatting can be done almost instantly. + + This change replaces black, isort, flake8 and flake8 plugins. + +- libvcs: 0.21.1 -> 0.21.2 (addition of ruff) +- poetry: 1.4.0 -> 1.5.0 + + See also: https://github.com/python-poetry/poetry/releases/tag/1.5.0 + +[ruff]: https://ruff.rs + +## vcspull v1.20.3 (2023-05-14) + +### Bug fix + +- Refactor `.types` imports to not require `typing-extensions` on production + builds (fixes #412 via #414, thank you for the report @Jasha10) + +## vcspull v1.20.2 (2023-04-07) + +_Maintenance only, no bug fixes or features_ + +### Development + +- Update mypy to 1.2.0 +- libvcs: 0.21.1 -> 0.21.2 + + Typing update only + +## vcspull v1.20.1 (2023-03-15) + +- libvcs: 0.21.0 -> 0.21.1 + + This removes even more `typing-extensions` runtime dependencies from + production builds. + +## vcspull v1.20.0 (2023-03-15) + +### Bug fix + +- libvcs: 0.20.0 -> 0.21.0 + + This removes the `typing-extensions` requirement from production builds. + +## vcspull v1.19.0 (2022-12-31) + +### Internal + +- mypy: Add `--strict` typings (#386) + +## vcspull v1.18.0 (2022-10-31) + +### Python 3.11 support (#409) + +- Bump libvcs 0.19.1 -> 0.20.0 + + Adds python 3.11 support + +## vcspull v1.17.1 (2022-10-23) + +**Maintenance release, no features or fixes** + +### Internal + +- Bump libvcs 0.19.0 -> 0.19.1 + + Doc fixes and test improvements + ## vcspull v1.17.0 (2022-10-23) **Maintenance release, no features or fixes** @@ -200,7 +597,13 @@ $ pipx install --suffix=@next 'vcspull' --pip-args '\--pre' --force - Move to `src/` directory structure (#382) - libvcs: Update to 0.17.x (#373) -- Basic mypy annotations (#373) +- mypy: + + - Basic mypy annotations (#373) + - [`mypy --strict`] compliant (#386) + + [`mypy --strict`]: https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-strict + - Remove `.pre-commit-config.yaml`: Let's not automate what the contributor could / should do themselves. - Add [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) (#379) @@ -641,7 +1044,7 @@ _✨ Renewed_ - [config/vcs]: Exception for private / ssh GitHub repository URLs and message to change to correct format. - [docs]: Update docs to reflect updates on new commands and how to clone - private / priveleged GitHub repos. + private / privileged GitHub repos. ## vcspull 0.0.8-4 (2014-02-06) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b60384c6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,247 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## CRITICAL REQUIREMENTS + +### Test Success +- ALL tests MUST pass for code to be considered complete and working +- Never describe code as "working as expected" if there are ANY failing tests +- Even if specific feature tests pass, failing tests elsewhere indicate broken functionality +- Changes that break existing tests must be fixed before considering implementation complete +- A successful implementation must pass linting, type checking, AND all existing tests + +## Project Overview + +vcspull is a Python tool for managing and synchronizing multiple git, svn, and mercurial repositories via YAML or JSON configuration files. It allows users to pull/update multiple repositories in a single command, optionally filtering by repository name, path, or VCS URL. + +## Development Environment + +### Setup and Installation + +```bash +# Install development dependencies with uv +uv pip install -e . +``` + +### Common Commands + +#### Testing + +```bash +# Run all tests +uv run pytest + +# Run specific test(s) +uv run pytest tests/test_cli.py +uv run pytest tests/test_cli.py::test_sync + +# Watch mode for tests (auto re-run on file changes) +uv run ptw . +# or +make start + +# Run tests with coverage +uv run py.test --cov -v +``` + +#### Code Quality + +```bash +# Format code with ruff +uv run ruff format . +# or +make ruff_format + +# Run ruff linting with auto-fixes +uv run ruff check . --fix --show-fixes +# or +make ruff + +# Run mypy type checking +uv run mypy +# or +make mypy + +# Watch mode for linting (using entr) +make watch_ruff +make watch_mypy +``` + +#### Documentation + +```bash +# Build documentation +make build_docs + +# Start documentation server (auto-reload) +make start_docs +``` + +## Development Process + +Follow this workflow for code changes: + +1. **Format First**: `uv run ruff format .` +2. **Run Tests**: `uv run py.test` +3. **Run Linting**: `uv run ruff check . --fix --show-fixes` +4. **Check Types**: `uv run mypy` +5. **Verify Tests Again**: `uv run py.test` + +## Code Architecture + +### Core Components + +1. **Configuration** + - `config.py`: Handles loading and parsing of YAML/JSON configuration files + - `_internal/config_reader.py`: Low-level config file reading + +2. **CLI** + - `cli/__init__.py`: Main CLI entry point with argument parsing + - `cli/sync.py`: Repository synchronization functionality + - `cli/add.py`: Adding new repositories to configuration + - `cli/add_from_fs.py`: Scanning filesystem for repositories + +3. **Repository Management** + - Uses `libvcs` package for VCS operations (git, svn, hg) + - Supports custom remotes and URL schemes + +### Configuration Format + +Configuration files are stored as YAML or JSON in either: +- `~/.vcspull.yaml`/`.json` (home directory) +- `~/.config/vcspull/` directory (XDG config) + +Example format: +```yaml +~/code/: + flask: "git+https://github.com/mitsuhiko/flask.git" +~/study/c: + awesome: "git+git://git.naquadah.org/awesome.git" +``` + +## Coding Standards + +### Imports + +- Use namespace imports: `import enum` instead of `from enum import Enum` +- For typing, use `import typing as t` and access via namespace: `t.NamedTuple`, etc. +- Use `from __future__ import annotations` at the top of all Python files + +### Docstrings + +Follow NumPy docstring style for all functions and methods: + +```python +"""Short description of the function or class. + +Detailed description using reStructuredText format. + +Parameters +---------- +param1 : type + Description of param1 +param2 : type + Description of param2 + +Returns +------- +type + Description of return value +""" +``` + +### Testing + +#### Using libvcs Fixtures + +When writing tests, leverage libvcs's pytest plugin fixtures: + +- `create_git_remote_repo`, `create_svn_remote_repo`, `create_hg_remote_repo`: Factory fixtures +- `git_repo`, `svn_repo`, `hg_repo`: Pre-made repository instances +- `set_home`, `gitconfig`, `hgconfig`, `git_commit_envvars`: Environment fixtures + +Example: +```python +def test_vcspull_sync(git_repo): + # git_repo is already a GitSync instance with a clean repository + # Use it directly in your tests +``` + +#### Test Structure + +Use `typing.NamedTuple` for parameterized tests: + +```python +class SyncFixture(t.NamedTuple): + test_id: str # For test naming + sync_args: list[str] + expected_exit_code: int + expected_in_out: ExpectedOutput = None + +@pytest.mark.parametrize( + list(SyncFixture._fields), + SYNC_REPO_FIXTURES, + ids=[test.test_id for test in SYNC_REPO_FIXTURES], +) +def test_sync( + # Parameters and fixtures... +): + # Test implementation +``` + +#### Mocking Strategy + +- Use `monkeypatch` for environment, globals, attributes +- Use `mocker` (from pytest-mock) for application code +- Document every mock with comments explaining WHAT is being mocked and WHY + +#### Configuration File Testing + +- Use project helper functions like `vcspull.tests.helpers.write_config` or `save_config_yaml` +- Avoid direct `yaml.dump` or `file.write_text` for config creation + +### Git Commit Standards + +Format commit messages as: +``` +Component/File(commit-type[Subcomponent/method]): Concise description + +why: Explanation of necessity or impact. +what: +- Specific technical changes made +- Focused on a single topic + +refs: #issue-number, breaking changes, or relevant links +``` + +Common commit types: +- **feat**: New features or enhancements +- **fix**: Bug fixes +- **refactor**: Code restructuring without functional change +- **docs**: Documentation updates +- **chore**: Maintenance (dependencies, tooling, config) +- **test**: Test-related updates +- **style**: Code style and formatting + +Example: +``` +cli/add(feat[add_repo]): Add support for custom remote URLs + +why: Enable users to specify alternative remote URLs for repositories +what: +- Add remote_url parameter to add_repo function +- Update CLI argument parser to accept --remote-url option +- Add tests for the new functionality + +refs: #123 +``` + +## Debugging Tips + +When stuck in debugging loops: + +1. **Pause and acknowledge the loop** +2. **Minimize to MVP**: Remove all debugging cruft and experimental code +3. **Document the issue** comprehensively for a fresh approach +4. Format for portability (using quadruple backticks) \ No newline at end of file diff --git a/Makefile b/Makefile index 7c950cae..c4deec20 100644 --- a/Makefile +++ b/Makefile @@ -8,20 +8,14 @@ entr_warn: @echo " ! File watching functionality non-operational ! " @echo " " @echo "Install entr(1) to automatically run tasks on file change." - @echo "See http://entrproject.org/ " + @echo "See https://eradman.com/entrproject/ " @echo "----------------------------------------------------------" -isort: - poetry run isort `${PY_FILES}` - -black: - poetry run black `${PY_FILES}` - test: - poetry run py.test $(test) + uv run py.test $(test) start: - $(MAKE) test; poetry run ptw . + $(MAKE) test; uv run ptw . watch_test: if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) test; else $(MAKE) test entr_warn; fi @@ -35,14 +29,17 @@ start_docs: design_docs: $(MAKE) -C docs design -flake8: - flake8 vcspull tests +ruff_format: + uv run ruff format . + +ruff: + uv run ruff check . -watch_flake8: - if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) flake8; else $(MAKE) flake8 entr_warn; fi +watch_ruff: + if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) ruff; else $(MAKE) ruff entr_warn; fi mypy: - poetry run mypy `${PY_FILES}` + uv run mypy `${PY_FILES}` watch_mypy: if command -v entr > /dev/null; then ${PY_FILES} | entr -c $(MAKE) mypy; else $(MAKE) mypy entr_warn; fi @@ -51,7 +48,7 @@ format_markdown: prettier --parser=markdown -w *.md docs/*.md docs/**/*.md CHANGES monkeytype_create: - poetry run monkeytype run `poetry run which py.test` + uv run monkeytype run `uv run which py.test` monkeytype_apply: - poetry run monkeytype list-modules | xargs -n1 -I{} sh -c 'poetry run monkeytype apply {}' + uv run monkeytype list-modules | xargs -n1 -I{} sh -c 'uv run monkeytype apply {}' diff --git a/README.md b/README.md index 76dea2d8..76e8bf23 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Your money will go directly to maintenance and development of the project. If you are an individual, feel free to give whatever feels right for the value you get out of the project. -See donation options at . +See donation options at . # More information diff --git a/conftest.py b/conftest.py index 7edd34fe..36e8cd3e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,15 +1,30 @@ -import pathlib +"""Conftest.py (root-level). + +We keep this in root pytest fixtures in pytest's doctest plugin to be available, as well +as avoiding conftest.py from being included in the wheel, in addition to pytest_plugin +for pytester only being available via the root directory. + +See "pytest_plugins in non-top-level conftest files" in +https://docs.pytest.org/en/stable/deprecations.html +""" + +from __future__ import annotations + import shutil import typing as t import pytest +if t.TYPE_CHECKING: + import pathlib + @pytest.fixture(autouse=True) def add_doctest_fixtures( request: pytest.FixtureRequest, - doctest_namespace: t.Dict[str, t.Any], + doctest_namespace: dict[str, t.Any], ) -> None: + """Harness pytest fixtures to doctests namespace.""" from _pytest.doctest import DoctestItem if isinstance(request._pyfuncitem, DoctestItem): @@ -24,28 +39,37 @@ def setup( set_home: pathlib.Path, xdg_config_path: pathlib.Path, ) -> None: - pass + """Automatically load the pytest fixtures in the parameters.""" @pytest.fixture(autouse=True) def cwd_default(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> None: + """Change the current directory to a temporary directory.""" monkeypatch.chdir(tmp_path) -@pytest.fixture(autouse=True, scope="session") -@pytest.mark.usefixtures("set_home") -def xdg_config_path(user_path: pathlib.Path): +@pytest.fixture(autouse=True) +def xdg_config_path( + user_path: pathlib.Path, + set_home: pathlib.Path, +) -> pathlib.Path: + """Create and return path to use for XDG Config Path.""" p = user_path / ".config" - p.mkdir() + if not p.exists(): + p.mkdir() return p -@pytest.fixture(scope="function") -def config_path(xdg_config_path: pathlib.Path, request: pytest.FixtureRequest): +@pytest.fixture +def config_path( + xdg_config_path: pathlib.Path, + request: pytest.FixtureRequest, +) -> pathlib.Path: + """Ensure and return vcspull configuration path.""" conf_path = xdg_config_path / "vcspull" conf_path.mkdir(exist_ok=True) - def clean(): + def clean() -> None: shutil.rmtree(conf_path) request.addfinalizer(clean) @@ -53,18 +77,22 @@ def clean(): @pytest.fixture(autouse=True) -def set_xdg_config_path(monkeypatch: pytest.MonkeyPatch, xdg_config_path: pathlib.Path): +def set_xdg_config_path( + monkeypatch: pytest.MonkeyPatch, + xdg_config_path: pathlib.Path, +) -> None: + """Set XDG_CONFIG_HOME environment variable.""" monkeypatch.setenv("XDG_CONFIG_HOME", str(xdg_config_path)) -@pytest.fixture(scope="function") -def repos_path(user_path: pathlib.Path, request: pytest.FixtureRequest): +@pytest.fixture +def repos_path(user_path: pathlib.Path, request: pytest.FixtureRequest) -> pathlib.Path: """Return temporary directory for repository checkout guaranteed unique.""" - dir = user_path / "repos" - dir.mkdir(exist_ok=True) + path = user_path / "repos" + path.mkdir(exist_ok=True) - def clean(): - shutil.rmtree(dir) + def clean() -> None: + shutil.rmtree(path) request.addfinalizer(clean) - return dir + return path diff --git a/docs/Makefile b/docs/Makefile index c19a7939..f50e525f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -182,8 +182,8 @@ dev: $(MAKE) -j watch serve start: - poetry run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} $(O) + uv run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} $(O) design: # This adds additional watch directories (for _static file changes) and disable incremental builds - poetry run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} --watch "." -a $(O) + uv run sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) --port ${HTTP_PORT} --watch "." -a $(O) diff --git a/docs/_templates/sidebar/projects.html b/docs/_templates/sidebar/projects.html index 330d1593..7b46e0bc 100644 --- a/docs/_templates/sidebar/projects.html +++ b/docs/_templates/sidebar/projects.html @@ -1,7 +1,7 @@