diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..e992fd2 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,23 @@ +# Changes here will be overwritten by Copier +_commit: 1.4.0 +_src_path: gh:pawamoy/copier-uv +author_email: dev@pawamoy.fr +author_fullname: Timothée Mazzucotelli +author_username: pawamoy +copyright_date: '2024' +copyright_holder: Timothée Mazzucotelli +copyright_holder_email: dev@pawamoy.fr +copyright_license: ISC License +insiders: true +insiders_email: insiders@pawamoy.fr +insiders_repository_name: griffe-autodocstringstyle +project_description: Set docstring style to 'auto' for external packages. +project_name: griffe-autodocstringstyle +public_release: false +python_package_command_line_name: '' +python_package_distribution_name: griffe-autodocstringstyle +python_package_import_name: griffe_autodocstringstyle +repository_name: griffe-autodocstringstyle +repository_namespace: mkdocstrings +repository_provider: github.com + diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..f9d77ee --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add scripts diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a502284 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +github: pawamoy +ko_fi: pawamoy +polar: pawamoy +custom: +- https://www.paypal.me/pawamoy diff --git a/.github/ISSUE_TEMPLATE/1-bug.md b/.github/ISSUE_TEMPLATE/1-bug.md new file mode 100644 index 0000000..a51595e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -0,0 +1,61 @@ +--- +name: Bug report +about: Create a bug report to help us improve. +title: "bug: " +labels: unconfirmed +assignees: [pawamoy] +--- + +### Description of the bug + + +### To Reproduce + + +``` +WRITE MRE / INSTRUCTIONS HERE +``` + +### Full traceback + + +
Full traceback + +```python +PASTE TRACEBACK HERE +``` + +
+ +### Expected behavior + + +### Environment information + + +```bash +python -m griffe_autodocstringstyle.debug # | xclip -selection clipboard +``` + +PASTE MARKDOWN OUTPUT HERE + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/2-feature.md b/.github/ISSUE_TEMPLATE/2-feature.md new file mode 100644 index 0000000..2df98fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project. +title: "feature: " +labels: feature +assignees: pawamoy +--- + +### Is your feature request related to a problem? Please describe. + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/3-docs.md b/.github/ISSUE_TEMPLATE/3-docs.md new file mode 100644 index 0000000..92ac8ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-docs.md @@ -0,0 +1,16 @@ +--- +name: Documentation update +about: Point at unclear, missing or outdated documentation. +title: "docs: " +labels: docs +assignees: pawamoy +--- + +### Is something unclear, missing or outdated in our documentation? + + +### Relevant code snippets + + +### Link to the relevant documentation section + diff --git a/.github/ISSUE_TEMPLATE/4-change.md b/.github/ISSUE_TEMPLATE/4-change.md new file mode 100644 index 0000000..dc9a8f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-change.md @@ -0,0 +1,18 @@ +--- +name: Change request +about: Suggest any other kind of change for this project. +title: "change: " +assignees: pawamoy +--- + +### Is your change request related to a problem? Please describe. + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ffeb1bc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: +- name: I have a question / I need help + url: https://github.com/mkdocstrings/griffe-autodocstringstyle/discussions/new?category=q-a + about: Ask and answer questions in the Discussions tab. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a401d10 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,121 @@ +name: ci + +on: + push: + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +env: + LANG: en_US.utf-8 + LC_ALL: en_US.utf-8 + PYTHONIOENCODING: UTF-8 + PYTHON_VERSIONS: "" + +jobs: + + quality: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Fetch all tags + run: git fetch --depth=1 --tags + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + run: pip install uv + + - name: Install dependencies + run: make setup + + - name: Check if the documentation builds correctly + run: make check-docs + + - name: Check the code quality + run: make check-quality + + - name: Check if the code is correctly typed + run: make check-types + + - name: Check for breaking changes in the API + run: make check-api + + exclude-test-jobs: + runs-on: ubuntu-latest + outputs: + jobs: ${{ steps.exclude-jobs.outputs.jobs }} + steps: + - id: exclude-jobs + run: | + if ${{ github.repository_owner == 'pawamoy-insiders' }}; then + echo 'jobs=[ + {"os": "macos-latest"}, + {"os": "windows-latest"}, + {"python-version": "3.9"}, + {"python-version": "3.10"}, + {"python-version": "3.11"}, + {"python-version": "3.12"}, + {"python-version": "3.13"} + ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT + else + echo 'jobs=[ + {"os": "macos-latest", "resolution": "lowest-direct"}, + {"os": "windows-latest", "resolution": "lowest-direct"} + ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT + fi + + tests: + + needs: exclude-test-jobs + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + resolution: + - highest + - lowest-direct + exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.python-version == '3.13' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Install uv + run: pip install uv + + - name: Install dependencies + env: + UV_RESOLUTION: ${{ matrix.resolution }} + run: make setup + + - name: Run the test suite + run: make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..17b8ca5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: release + +on: push +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Fetch all tags + run: git fetch --depth=1 --tags + - name: Setup Python + uses: actions/setup-python@v4 + - name: Install build + if: github.repository_owner == 'pawamoy-insiders' + run: python -m pip install build + - name: Build dists + if: github.repository_owner == 'pawamoy-insiders' + run: python -m build + - name: Upload dists artifact + uses: actions/upload-artifact@v4 + if: github.repository_owner == 'pawamoy-insiders' + with: + name: griffe-autodocstringstyle-insiders + path: ./dist/* + - name: Install git-changelog + if: github.repository_owner != 'pawamoy-insiders' + run: pip install git-changelog + - name: Prepare release notes + if: github.repository_owner != 'pawamoy-insiders' + run: git-changelog --release-notes > release-notes.md + - name: Create release with assets + uses: softprops/action-gh-release@v1 + if: github.repository_owner == 'pawamoy-insiders' + with: + files: ./dist/* + - name: Create release + uses: softprops/action-gh-release@v1 + if: github.repository_owner != 'pawamoy-insiders' + with: + body_path: release-notes.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41fee62 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# editors +.idea/ +.vscode/ + +# python +*.egg-info/ +*.py[cod] +.venv/ +.venvs/ +/build/ +/dist/ + +# tools +.coverage* +/.pdm-build/ +/htmlcov/ +/site/ + +# cache +.cache/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +__pycache__/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile new file mode 100644 index 0000000..1590b41 --- /dev/null +++ b/.gitpod.dockerfile @@ -0,0 +1,6 @@ +FROM gitpod/workspace-full +USER gitpod +ENV PIP_USER=no +RUN pip3 install pipx; \ + pipx install uv; \ + pipx ensurepath diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..23a3c2b --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,13 @@ +vscode: + extensions: + - ms-python.python + +image: + file: .gitpod.dockerfile + +ports: +- port: 8000 + onOpen: notify + +tasks: +- init: make setup diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a87281b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..255e0ee --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +dev@pawamoy.fr. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..905075c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,149 @@ +# Contributing + +Contributions are welcome, and they are greatly appreciated! +Every little bit helps, and credit will always be given. + +## Environment setup + +Nothing easier! + +Fork and clone the repository, then: + +```bash +cd griffe-autodocstringstyle +make setup +``` + +> NOTE: +> If it fails for some reason, +> you'll need to install +> [uv](https://github.com/astral-sh/uv) +> manually. +> +> You can install it with: +> +> ```bash +> python3 -m pip install --user pipx +> pipx install uv +> ``` +> +> Now you can try running `make setup` again, +> or simply `uv install`. + +You now have the dependencies installed. + +Run `make help` to see all the available actions! + +## Tasks + +The entry-point to run commands and tasks is the `make` Python script, +located in the `scripts` directory. Try running `make` to show the available commands and tasks. +The *commands* do not need the Python dependencies to be installed, +while the *tasks* do. +The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). + +If you work in VSCode, we provide +[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) +for the project. + +## Development + +As usual: + +1. create a new branch: `git switch -c feature-or-bugfix-name` +1. edit the code and/or the documentation + +**Before committing:** + +1. run `make format` to auto-format the code +1. run `make check` to check everything (fix any warning) +1. run `make test` to run the tests (fix any issue) +1. if you updated the documentation or the project dependencies: + 1. run `make docs` + 1. go to http://localhost:8000 and check that everything looks good +1. follow our [commit message convention](#commit-message-convention) + +If you are unsure about how to fix or ignore a warning, +just let the continuous integration fail, +and we will help you during review. + +Don't bother updating the changelog, we will take care of this. + +## Commit message convention + +Commit messages must follow our convention based on the +[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) +or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): + +``` +[(scope)]: Subject + +[Body] +``` + +**Subject and body must be valid Markdown.** +Subject must have proper casing (uppercase for first letter +if it makes sense), but no dot at the end, and no punctuation +in general. + +Scope and body are optional. Type can be: + +- `build`: About packaging, building wheels, etc. +- `chore`: About packaging or repo/files management. +- `ci`: About Continuous Integration. +- `deps`: Dependencies update. +- `docs`: About documentation. +- `feat`: New feature. +- `fix`: Bug fix. +- `perf`: About performance. +- `refactor`: Changes that are not features or bug fixes. +- `style`: A change in code style/format. +- `tests`: About tests. + +If you write a body, please add trailers at the end +(for example issues and PR references, or co-authors), +without relying on GitHub's flavored Markdown: + +``` +Body. + +Issue #10: https://github.com/namespace/project/issues/10 +Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15 +``` + +These "trailers" must appear at the end of the body, +without any blank lines between them. The trailer title +can contain any character except colons `:`. +We expect a full URI for each trailer, not just GitHub autolinks +(for example, full GitHub URLs for commits and issues, +not the hash or the #issue-number). + +We do not enforce a line length on commit messages summary and body, +but please avoid very long summaries, and very long lines in the body, +unless they are part of code blocks that must not be wrapped. + +## Pull requests guidelines + +Link to any related issue in the Pull Request message. + +During the review, we recommend using fixups: + +```bash +# SHA is the SHA of the commit you want to fix +git commit --fixup=SHA +``` + +Once all the changes are approved, you can squash your commits: + +```bash +git rebase -i --autosquash main +``` + +And force-push: + +```bash +git push -f +``` + +If this seems all too complicated, you can push or force-push each new commit, +and we will squash them ourselves if needed, before merging. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c8325e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2024, Timothée Mazzucotelli + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5e88121 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# If you have `direnv` loaded in your shell, and allow it in the repository, +# the `make` command will point at the `scripts/make` shell script. +# This Makefile is just here to allow auto-completion in the terminal. + +actions = \ + allrun \ + changelog \ + check \ + check-api \ + check-docs \ + check-quality \ + check-types \ + clean \ + coverage \ + docs \ + docs-deploy \ + format \ + help \ + multirun \ + release \ + run \ + setup \ + test \ + vscode + +.PHONY: $(actions) +$(actions): + @python scripts/make "$@" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8deb69 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# griffe-autodocstringstyle + +[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/griffe-autodocstringstyle/) +[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/griffe-autodocstringstyle) +[![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#griffe-autodocstringstyle:gitter.im) + +Set docstring style to 'auto' for external packages. + +## Installation + +This project is available to sponsors only, through my Insiders program. +See Insiders [explanation](https://mkdocstrings.github.io/griffe-autodocstringstyle/insiders/) +and [installation instructions](https://mkdocstrings.github.io/griffe-autodocstringstyle/insiders/installation/). diff --git a/config/coverage.ini b/config/coverage.ini new file mode 100644 index 0000000..b56a286 --- /dev/null +++ b/config/coverage.ini @@ -0,0 +1,25 @@ +[coverage:run] +branch = true +parallel = true +source = + src/ + tests/ + +[coverage:paths] +equivalent = + src/ + .venv/lib/*/site-packages/ + .venvs/*/lib/*/site-packages/ + +[coverage:report] +precision = 2 +omit = + src/*/__init__.py + src/*/__main__.py + tests/__init__.py +exclude_lines = + pragma: no cover + if TYPE_CHECKING + +[coverage:json] +output = htmlcov/coverage.json diff --git a/config/git-changelog.toml b/config/git-changelog.toml new file mode 100644 index 0000000..57114e0 --- /dev/null +++ b/config/git-changelog.toml @@ -0,0 +1,9 @@ +bump = "auto" +convention = "angular" +in-place = true +output = "CHANGELOG.md" +parse-refs = false +parse-trailers = true +sections = ["build", "deps", "feat", "fix", "refactor"] +template = "keepachangelog" +versioning = "pep440" diff --git a/config/mypy.ini b/config/mypy.ini new file mode 100644 index 0000000..814e2ac --- /dev/null +++ b/config/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = true +exclude = tests/fixtures/ +warn_unused_ignores = true +show_error_codes = true diff --git a/config/pytest.ini b/config/pytest.ini new file mode 100644 index 0000000..052a2f1 --- /dev/null +++ b/config/pytest.ini @@ -0,0 +1,14 @@ +[pytest] +python_files = + test_*.py +addopts = + --cov + --cov-config config/coverage.ini +testpaths = + tests + +# action:message_regex:warning_class:module_regex:line +filterwarnings = + error + # TODO: remove once pytest-xdist 4 is released + ignore:.*rsyncdir:DeprecationWarning:xdist diff --git a/config/ruff.toml b/config/ruff.toml new file mode 100644 index 0000000..5cb62d4 --- /dev/null +++ b/config/ruff.toml @@ -0,0 +1,84 @@ +target-version = "py38" +line-length = 120 + +[lint] +exclude = [ + "tests/fixtures/*.py", +] +select = [ + "A", "ANN", "ARG", + "B", "BLE", + "C", "C4", + "COM", + "D", "DTZ", + "E", "ERA", "EXE", + "F", "FBT", + "G", + "I", "ICN", "INP", "ISC", + "N", + "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", + "Q", + "RUF", "RSE", "RET", + "S", "SIM", "SLF", + "T", "T10", "T20", "TCH", "TID", "TRY", + "UP", + "W", + "YTT", +] +ignore = [ + "A001", # Variable is shadowing a Python builtin + "ANN101", # Missing type annotation for self + "ANN102", # Missing type annotation for cls + "ANN204", # Missing return type annotation for special method __str__ + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "ARG005", # Unused lambda argument + "C901", # Too complex + "D105", # Missing docstring in magic method + "D417", # Missing argument description in the docstring + "E501", # Line too long + "ERA001", # Commented out code + "G004", # Logging statement uses f-string + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments to function call + "PLR0915", # Too many statements + "SLF001", # Private member accessed + "TRY003", # Avoid specifying long messages outside the exception class +] + +[lint.per-file-ignores] +"src/*/cli.py" = [ + "T201", # Print statement +] +"src/*/debug.py" = [ + "T201", # Print statement +] +"scripts/*.py" = [ + "INP001", # File is part of an implicit namespace package + "T201", # Print statement +] +"tests/*.py" = [ + "ARG005", # Unused lambda argument + "FBT001", # Boolean positional arg in function definition + "PLR2004", # Magic value used in comparison + "S101", # Use of assert detected +] + +[lint.flake8-quotes] +docstring-quotes = "double" + +[lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[lint.isort] +known-first-party = ["griffe_autodocstringstyle"] + +[lint.pydocstyle] +convention = "google" + +[format] +exclude = [ + "tests/fixtures/*.py", +] +docstring-code-format = true +docstring-code-line-length = 80 diff --git a/config/vscode/launch.json b/config/vscode/launch.json new file mode 100644 index 0000000..e328838 --- /dev/null +++ b/config/vscode/launch.json @@ -0,0 +1,47 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "python (current file)", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "docs", + "type": "debugpy", + "request": "launch", + "module": "mkdocs", + "justMyCode": false, + "args": [ + "serve", + "-v" + ] + }, + { + "name": "test", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "justMyCode": false, + "args": [ + "-c=config/pytest.ini", + "-vvv", + "--no-cov", + "--dist=no", + "tests", + "-k=${input:tests_selection}" + ] + } + ], + "inputs": [ + { + "id": "tests_selection", + "type": "promptString", + "description": "Tests selection", + "default": "" + } + ] +} \ No newline at end of file diff --git a/config/vscode/settings.json b/config/vscode/settings.json new file mode 100644 index 0000000..949856d --- /dev/null +++ b/config/vscode/settings.json @@ -0,0 +1,33 @@ +{ + "files.watcherExclude": { + "**/.venv*/**": true, + "**/.venvs*/**": true, + "**/venv*/**": true + }, + "mypy-type-checker.args": [ + "--config-file=config/mypy.ini" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "--config-file=config/pytest.ini" + ], + "ruff.enable": true, + "ruff.format.args": [ + "--config=config/ruff.toml" + ], + "ruff.lint.args": [ + "--config=config/ruff.toml" + ], + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", + "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" + ] +} \ No newline at end of file diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json new file mode 100644 index 0000000..73145ee --- /dev/null +++ b/config/vscode/tasks.json @@ -0,0 +1,97 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "changelog", + "type": "process", + "command": "scripts/make", + "args": ["changelog"] + }, + { + "label": "check", + "type": "process", + "command": "scripts/make", + "args": ["check"] + }, + { + "label": "check-quality", + "type": "process", + "command": "scripts/make", + "args": ["check-quality"] + }, + { + "label": "check-types", + "type": "process", + "command": "scripts/make", + "args": ["check-types"] + }, + { + "label": "check-docs", + "type": "process", + "command": "scripts/make", + "args": ["check-docs"] + }, + { + "label": "check-api", + "type": "process", + "command": "scripts/make", + "args": ["check-api"] + }, + { + "label": "clean", + "type": "process", + "command": "scripts/make", + "args": ["clean"] + }, + { + "label": "docs", + "type": "process", + "command": "scripts/make", + "args": ["docs"] + }, + { + "label": "docs-deploy", + "type": "process", + "command": "scripts/make", + "args": ["docs-deploy"] + }, + { + "label": "format", + "type": "process", + "command": "scripts/make", + "args": ["format"] + }, + { + "label": "release", + "type": "process", + "command": "scripts/make", + "args": ["release", "${input:version}"] + }, + { + "label": "setup", + "type": "process", + "command": "scripts/make", + "args": ["setup"] + }, + { + "label": "test", + "type": "process", + "command": "scripts/make", + "args": ["test", "coverage"], + "group": "test" + }, + { + "label": "vscode", + "type": "process", + "command": "scripts/make", + "args": ["vscode"] + } + ], + "inputs": [ + { + "id": "version", + "type": "promptString", + "description": "Version" + } + ] +} \ No newline at end of file diff --git a/devdeps.txt b/devdeps.txt new file mode 100644 index 0000000..e0afd7e --- /dev/null +++ b/devdeps.txt @@ -0,0 +1,32 @@ +# dev +editables>=0.5 + +# maintenance +build>=1.2 +git-changelog>=2.5 +twine>=5.0; python_version < '3.13' + +# ci +duty>=1.4 +ruff>=0.4 +pytest>=8.2 +pytest-cov>=5.0 +pytest-randomly>=3.15 +pytest-xdist>=3.6 +mypy>=1.10 +types-markdown>=3.6 +types-pyyaml>=6.0 + +# docs +black>=24.4 +markdown-callouts>=0.4 +markdown-exec>=1.8 +mkdocs>=1.6 +mkdocs-coverage>=1.0 +mkdocs-gen-files>=0.5 +mkdocs-git-committers-plugin-2>=2.3 +mkdocs-literate-nav>=0.6 +mkdocs-material>=9.5 +mkdocs-minify-plugin>=0.8 +mkdocstrings[python]>=0.25 +tomli>=2.0; python_version < '3.11' diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html new file mode 100644 index 0000000..1e95685 --- /dev/null +++ b/docs/.overrides/main.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block announce %} + + Fund this project through + sponsorship + + {% include ".icons/octicons/heart-fill-16.svg" %} + — + + Follow + @pawamoy on + + + {% include ".icons/fontawesome/brands/mastodon.svg" %} + + Fosstodon + + for updates +{% endblock %} diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html new file mode 100644 index 0000000..8a870ee --- /dev/null +++ b/docs/.overrides/partials/comments.html @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..786b75d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md new file mode 100644 index 0000000..01f2ea2 --- /dev/null +++ b/docs/code_of_conduct.md @@ -0,0 +1 @@ +--8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..ea38c9b --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1 @@ +--8<-- "CONTRIBUTING.md" diff --git a/docs/credits.md b/docs/credits.md new file mode 100644 index 0000000..f758db8 --- /dev/null +++ b/docs/credits.md @@ -0,0 +1,10 @@ +--- +hide: +- toc +--- + + +```python exec="yes" +--8<-- "scripts/gen_credits.py" +``` + diff --git a/docs/css/insiders.css b/docs/css/insiders.css new file mode 100644 index 0000000..e7b9c74 --- /dev/null +++ b/docs/css/insiders.css @@ -0,0 +1,124 @@ +@keyframes heart { + + 0%, + 40%, + 80%, + 100% { + transform: scale(1); + } + + 20%, + 60% { + transform: scale(1.15); + } +} + +@keyframes vibrate { + 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% { + -webkit-transform: translate3d(-2px, 0, 0); + transform: translate3d(-2px, 0, 0); + } + 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% { + -webkit-transform: translate3d(2px, 0, 0); + transform: translate3d(2px, 0, 0); + } + 20%, 100% { + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} + +.heart { + color: #e91e63; +} + +.pulse { + animation: heart 1000ms infinite; +} + +.vibrate { + animation: vibrate 2000ms infinite; +} + +.new-feature svg { + fill: var(--md-accent-fg-color) !important; +} + +a.insiders { + color: #e91e63; +} + +.sponsorship-list { + width: 100%; +} + +.sponsorship-item { + border-radius: 100%; + display: inline-block; + height: 1.6rem; + margin: 0.1rem; + overflow: hidden; + width: 1.6rem; +} + +.sponsorship-item:focus, .sponsorship-item:hover { + transform: scale(1.1); +} + +.sponsorship-item img { + filter: grayscale(100%) opacity(75%); + height: auto; + width: 100%; +} + +.sponsorship-item:focus img, .sponsorship-item:hover img { + filter: grayscale(0); +} + +.sponsorship-item.private { + background: var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color); + font-size: .6rem; + font-weight: 700; + line-height: 1.6rem; + text-align: center; +} + +.mastodon { + color: #897ff8; + border-radius: 100%; + box-shadow: inset 0 0 0 .05rem currentcolor; + display: inline-block; + height: 1.2rem !important; + padding: .25rem; + transition: all .25s; + vertical-align: bottom !important; + width: 1.2rem; +} + +.premium-sponsors { + text-align: center; +} + +#silver-sponsors img { + height: 140px; +} + +#bronze-sponsors img { + height: 140px; +} + +#bronze-sponsors p { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +#bronze-sponsors a { + display: block; + flex-shrink: 0; +} + +.sponsors-total { + font-weight: bold; +} \ No newline at end of file diff --git a/docs/css/material.css b/docs/css/material.css new file mode 100644 index 0000000..9e8c14a --- /dev/null +++ b/docs/css/material.css @@ -0,0 +1,4 @@ +/* More space at the bottom of the page. */ +.md-main__inner { + margin-bottom: 1.5rem; +} diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css new file mode 100644 index 0000000..88c7357 --- /dev/null +++ b/docs/css/mkdocstrings.css @@ -0,0 +1,27 @@ +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such. */ +a.external::after, +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + vertical-align: middle; + position: relative; + + height: 1em; + width: 1em; + background-color: currentColor; +} + +a.external:hover::after, +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..8e6f2fb --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +--- +hide: +- feedback +--- + +--8<-- "README.md" diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md new file mode 100644 index 0000000..4e1321e --- /dev/null +++ b/docs/insiders/changelog.md @@ -0,0 +1,7 @@ +# Changelog + +## griffe-autodocstringstyle Insiders + +### 1.0.0 April 22, 2023 { id="1.0.0" } + +- Release first Insiders version diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml new file mode 100644 index 0000000..0e27b99 --- /dev/null +++ b/docs/insiders/goals.yml @@ -0,0 +1,13 @@ +goals: + 500: + name: PlasmaVac User Guide + features: [] + 1000: + name: GraviFridge Fluid Renewal + features: [] + 1500: + name: HyperLamp Navigation Tips + features: [] + 2000: + name: FusionDrive Ejection Configuration + features: [] diff --git a/docs/insiders/index.md b/docs/insiders/index.md new file mode 100644 index 0000000..a3db081 --- /dev/null +++ b/docs/insiders/index.md @@ -0,0 +1,239 @@ +# Insiders + +*griffe-autodocstringstyle* follows the **sponsorware** release strategy, which means +that new features are first exclusively released to sponsors as part of +[Insiders][insiders]. Read on to learn [what sponsorships achieve][sponsorship], +[how to become a sponsor][sponsors] to get access to Insiders, +and [what's in it for you][features]! + +## What is Insiders? + +*griffe-autodocstringstyle Insiders* is a private fork of *griffe-autodocstringstyle*, hosted as +a private GitHub repository. Almost[^1] [all new features][features] +are developed as part of this fork, which means that they are immediately +available to all eligible sponsors, as they are made collaborators of this +repository. + + [^1]: + In general, every new feature is first exclusively released to sponsors, but + sometimes upstream dependencies enhance + existing features that must be supported by *griffe-autodocstringstyle*. + +Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a +funding goal is hit, the features that are tied to it are merged back into +*griffe-autodocstringstyle* and released for general availability, making them available +to all users. Bugfixes are always released in tandem. + +Sponsorships start as low as [**$10 a month**][sponsors].[^2] + + [^2]: + Note that $10 a month is the minimum amount to become eligible for + Insiders. While GitHub Sponsors also allows to sponsor lower amounts or + one-time amounts, those can't be granted access to Insiders due to + technical reasons. Such contributions are still very much welcome as + they help ensuring the project's sustainability. + + +## What sponsorships achieve + +Sponsorships make this project sustainable, as they buy the maintainers of this +project time – a very scarce resource – which is spent on the development of new +features, bug fixing, stability improvement, issue triage and general support. +The biggest bottleneck in Open Source is time.[^3] + + [^3]: + Making an Open Source project sustainable is exceptionally hard: maintainers + burn out, projects are abandoned. That's not great and very unpredictable. + The sponsorware model ensures that if you decide to use *griffe-autodocstringstyle*, + you can be sure that bugs are fixed quickly and new features are added + regularly. + +If you're unsure if you should sponsor this project, check out the list of +[completed funding goals][goals completed] to learn whether you're already using features that +were developed with the help of sponsorships. You're most likely using at least +a handful of them, [thanks to our awesome sponsors][sponsors]! + +## What's in it for me? + +```python exec="1" session="insiders" +data_source = "docs/insiders/goals.yml" +``` + + +```python exec="1" session="insiders" idprefix="" +--8<-- "scripts/insiders.py" + +if unreleased_features: + print( + "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get **immediate " + f"access to {len(unreleased_features)} additional features** that you can start using right away, and " + "which are currently exclusively available to sponsors:\n" + ) + + for feature in unreleased_features: + feature.render(badge=True) + + print( + "\n\nThese are just the features related to this project. " + "[See the complete feature list on the author's main Insiders page](https://pawamoy.github.io/insiders/#whats-in-it-for-me)." + ) +else: + print( + "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get immediate " + "access to all released features that you can start using right away, and " + "which are exclusively available to sponsors. At this moment, there are no " + "Insiders features for this project, but checkout the [next funding goals](#goals) " + "to see what's coming, as well as **[the feature list for all Insiders projects](https://pawamoy.github.io/insiders/#whats-in-it-for-me).**" + ) +``` + + +## How to become a sponsor + +Thanks for your interest in sponsoring! In order to become an eligible sponsor +with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], +and complete a sponsorship of **$10 a month or more**. +You can use your individual or organization GitHub account for sponsoring. + +Sponsorships lower than $10 a month are also very much appreciated, and useful. +They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. +*Every* sponsorship helps us implementing new features and releasing them to the public. + +**Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** +through a GitHub organization, please send a short email +to insiders@pawamoy.fr with the name of your +organization and the GitHub account of the individual +that should be added as a collaborator.[^4] + +You can cancel your sponsorship anytime.[^5] + + [^4]: + It's currently not possible to grant access to each member of an + organization, as GitHub only allows for adding users. Thus, after + sponsoring, please send an email to insiders@pawamoy.fr, stating which + account should become a collaborator of the Insiders repository. We're + working on a solution which will make access to organizations much simpler. + To ensure that access is not tied to a particular individual GitHub account, + create a bot account (i.e. a GitHub account that is not tied to a specific + individual), and use this account for the sponsoring. After being added to + the list of collaborators, the bot account can create a private fork of the + private Insiders GitHub repository, and grant access to all members of the + organizations. + + [^5]: + If you cancel your sponsorship, GitHub schedules a cancellation request + which will become effective at the end of the billing cycle. This means + that even though you cancel your sponsorship, you will keep your access to + Insiders as long as your cancellation isn't effective. All charges are + processed by GitHub through Stripe. As we don't receive any information + regarding your payment, and GitHub doesn't offer refunds, sponsorships are + non-refundable. + + +[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary } + +
+
+
+
+
+
+
+ +
+ + + If you sponsor publicly, you're automatically added here with a link to + your profile and avatar to show your support for *griffe-autodocstringstyle*. + Alternatively, if you wish to keep your sponsorship private, you'll be a + silent +1. You can select visibility during checkout and change it + afterwards. + + +## Funding + +### Goals + +The following section lists all funding goals. Each goal contains a list of +features prefixed with a checkmark symbol, denoting whether a feature is +:octicons-check-circle-fill-24:{ style="color: #00e676" } already available or +:octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, +but not yet implemented. When the funding goal is hit, +the features are released for general availability. + +```python exec="1" session="insiders" idprefix="" +for goal in goals.values(): + if not goal.complete: + goal.render() +``` + +### Goals completed + +This section lists all funding goals that were previously completed, which means +that those features were part of Insiders, but are now generally available and +can be used by all users. + +```python exec="1" session="insiders" idprefix="" +for goal in goals.values(): + if goal.complete: + goal.render() +``` + +## Frequently asked questions + +### Compatibility + +> We're building an open source project and want to allow outside collaborators +to use *griffe-autodocstringstyle* locally without having access to Insiders. +Is this still possible? + +Yes. Insiders is compatible with *griffe-autodocstringstyle*. Almost all new features +and configuration options are either backward-compatible or implemented behind +feature flags. Most Insiders features enhance the overall experience, +though while these features add value for the users of your project, they +shouldn't be necessary for previewing when making changes to content. + +### Payment + +> We don't want to pay for sponsorship every month. Are there any other options? + +Yes. You can sponsor on a yearly basis by [switching your GitHub account to a +yearly billing cycle][billing cycle]. If for some reason you cannot do that, you +could also create a dedicated GitHub account with a yearly billing cycle, which +you only use for sponsoring (some sponsors already do that). + +If you have any problems or further questions, please reach out to insiders@pawamoy.fr. + +### Terms + +> Are we allowed to use Insiders under the same terms and conditions as +*griffe-autodocstringstyle*? + +Yes. Whether you're an individual or a company, you may use *griffe-autodocstringstyle +Insiders* precisely under the same terms as *griffe-autodocstringstyle*, which are given +by the [ISC License][license]. However, we kindly ask you to respect our +**fair use policy**: + +- Please **don't distribute the source code** of Insiders. You may freely use + it for public, private or commercial projects, privately fork or mirror it, + but please don't make the source code public, as it would counteract the + sponsorware strategy. + +- If you cancel your subscription, you're automatically removed as a + collaborator and will miss out on all future updates of Insiders. However, you + may **use the latest version** that's available to you **as long as you like**. + Just remember that [GitHub deletes private forks][private forks]. + +[insiders]: #what-is-insiders +[sponsorship]: #what-sponsorships-achieve +[sponsors]: #how-to-become-a-sponsor +[features]: #whats-in-it-for-me +[funding]: #funding +[goals completed]: #goals-completed +[github sponsor profile]: https://github.com/sponsors/pawamoy +[billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle +[license]: ../license.md +[private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository + + + diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md new file mode 100644 index 0000000..0f5e8f8 --- /dev/null +++ b/docs/insiders/installation.md @@ -0,0 +1,88 @@ +--- +title: Getting started with Insiders +--- + +# Getting started with Insiders + +*griffe-autodocstringstyle Insiders* is a compatible drop-in replacement for *griffe-autodocstringstyle*, +and can be installed similarly using `pip` or `git`. +Note that in order to access the Insiders repository, +you need to [become an eligible sponsor] of @pawamoy on GitHub. + + [become an eligible sponsor]: index.md#how-to-become-a-sponsor + +## Installation + +### with PyPI Insiders + +[PyPI Insiders](https://pawamoy.github.io/pypi-insiders/) +is a tool that helps you keep up-to-date versions +of Insiders projects in the PyPI index of your choice +(self-hosted, Google registry, Artifactory, etc.). + +See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) +and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). + +**We kindly ask that you do not upload the distributions to public registries, +as it is against our [Terms of use](index.md#terms).** + +### with pip (ssh/https) + +*griffe-autodocstringstyle Insiders* can be installed with `pip` [using SSH][using ssh]: + +```bash +pip install git+ssh://git@github.com/pawamoy-insiders/griffe-autodocstringstyle.git +``` + + [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh + +Or using HTTPS: + +```bash +pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/griffe-autodocstringstyle.git +``` + +>? NOTE: **How to get a GitHub personal access token** +> The `GH_TOKEN` environment variable is a GitHub token. +> It can be obtained by creating a [personal access token] for +> your GitHub account. It will give you access to the Insiders repository, +> programmatically, from the command line or GitHub Actions workflows: +> +> 1. Go to https://github.com/settings/tokens +> 2. Click on [Generate a new token] +> 3. Enter a name and select the [`repo`][scopes] scope +> 4. Generate the token and store it in a safe place +> +> [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token +> [Generate a new token]: https://github.com/settings/tokens/new +> [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes +> +> Note that the personal access +> token must be kept secret at all times, as it allows the owner to access your +> private repositories. + +### with Git + +Of course, you can use *griffe-autodocstringstyle Insiders* directly using Git: + +``` +git clone git@github.com:pawamoy-insiders/griffe-autodocstringstyle +``` + +When cloning with Git, the package must be installed: + +``` +pip install -e griffe-autodocstringstyle +``` + +## Upgrading + +When upgrading Insiders, you should always check the version of *griffe-autodocstringstyle* +which makes up the first part of the version qualifier. For example, a version like +`8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`. + +If the major version increased, it's a good idea to consult the [changelog] +and go through the steps to ensure your configuration is up to date and +all necessary changes have been made. + + [changelog]: ./changelog.md diff --git a/docs/js/feedback.js b/docs/js/feedback.js new file mode 100644 index 0000000..f97321a --- /dev/null +++ b/docs/js/feedback.js @@ -0,0 +1,14 @@ +const feedback = document.forms.feedback; +feedback.hidden = false; + +feedback.addEventListener("submit", function(ev) { + ev.preventDefault(); + const commentElement = document.getElementById("feedback"); + commentElement.style.display = "block"; + feedback.firstElementChild.disabled = true; + const data = ev.submitter.getAttribute("data-md-value"); + const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']"); + if (note) { + note.hidden = false; + } +}) diff --git a/docs/js/insiders.js b/docs/js/insiders.js new file mode 100644 index 0000000..8bb6848 --- /dev/null +++ b/docs/js/insiders.js @@ -0,0 +1,74 @@ +function humanReadableAmount(amount) { + const strAmount = String(amount); + if (strAmount.length >= 4) { + return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`; + } + return strAmount; +} + +function getJSON(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'json'; + xhr.onload = function () { + var status = xhr.status; + if (status === 200) { + callback(null, xhr.response); + } else { + callback(status, xhr.response); + } + }; + xhr.send(); +} + +function updatePremiumSponsors(dataURL, rank) { + let capRank = rank.charAt(0).toUpperCase() + rank.slice(1); + getJSON(dataURL + `/sponsors${capRank}.json`, function (err, sponsors) { + const sponsorsDiv = document.getElementById(`${rank}-sponsors`); + if (sponsors.length > 0) { + let html = ''; + html += `${capRank} sponsors

` + sponsors.forEach(function (sponsor) { + html += ` + + ${sponsor.name} + + ` + }); + html += '

' + sponsorsDiv.innerHTML = html; + } + }); +} + +function updateInsidersPage(author_username) { + const sponsorURL = `https://github.com/sponsors/${author_username}` + const dataURL = `https://raw.githubusercontent.com/${author_username}/sponsors/main`; + getJSON(dataURL + '/numbers.json', function (err, numbers) { + document.getElementById('sponsors-count').innerHTML = numbers.count; + Array.from(document.getElementsByClassName('sponsors-total')).forEach(function (element) { + element.innerHTML = '$ ' + humanReadableAmount(numbers.total); + }); + getJSON(dataURL + '/sponsors.json', function (err, sponsors) { + const sponsorsElem = document.getElementById('sponsors'); + const privateSponsors = numbers.count - sponsors.length; + sponsors.forEach(function (sponsor) { + sponsorsElem.innerHTML += ` + + + + `; + }); + if (privateSponsors > 0) { + sponsorsElem.innerHTML += ` + + +${privateSponsors} + + `; + } + }); + }); + updatePremiumSponsors(dataURL, "gold"); + updatePremiumSponsors(dataURL, "silver"); + updatePremiumSponsors(dataURL, "bronze"); +} diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..e81c0ed --- /dev/null +++ b/docs/license.md @@ -0,0 +1,10 @@ +--- +hide: +- feedback +--- + +# License + +``` +--8<-- "LICENSE" +``` diff --git a/duties.py b/duties.py new file mode 100644 index 0000000..4ed2d1f --- /dev/null +++ b/duties.py @@ -0,0 +1,219 @@ +"""Development tasks.""" + +from __future__ import annotations + +import os +import sys +from contextlib import contextmanager +from importlib.metadata import version as pkgversion +from pathlib import Path +from typing import TYPE_CHECKING, Iterator + +from duty import duty, tools + +if TYPE_CHECKING: + from duty.context import Context + + +PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) +PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) +PY_SRC = " ".join(PY_SRC_LIST) +CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} +WINDOWS = os.name == "nt" +PTY = not WINDOWS and not CI +MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" + + +def pyprefix(title: str) -> str: # noqa: D103 + if MULTIRUN: + prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" + return f"{prefix:14}{title}" + return title + + +@contextmanager +def material_insiders() -> Iterator[bool]: # noqa: D103 + if "+insiders" in pkgversion("mkdocs-material"): + os.environ["MATERIAL_INSIDERS"] = "true" + try: + yield True + finally: + os.environ.pop("MATERIAL_INSIDERS") + else: + yield False + + +@duty +def changelog(ctx: Context, bump: str = "") -> None: + """Update the changelog in-place with latest commits. + + Parameters: + bump: Bump option passed to git-changelog. + """ + ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") + + +@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) +def check(ctx: Context) -> None: # noqa: ARG001 + """Check it all!""" + + +@duty +def check_quality(ctx: Context) -> None: + """Check the code quality.""" + ctx.run( + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), + title=pyprefix("Checking code quality"), + ) + + +@duty +def check_docs(ctx: Context) -> None: + """Check if the documentation builds correctly.""" + Path("htmlcov").mkdir(parents=True, exist_ok=True) + Path("htmlcov/index.html").touch(exist_ok=True) + with material_insiders(): + ctx.run( + tools.mkdocs.build(strict=True, verbose=True), + title=pyprefix("Building documentation"), + ) + + +@duty +def check_types(ctx: Context) -> None: + """Check that the code is correctly typed.""" + ctx.run( + tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), + title=pyprefix("Type-checking"), + ) + + +@duty +def check_api(ctx: Context, *cli_args: str) -> None: + """Check for API breaking changes.""" + ctx.run( + tools.griffe.check("griffe_autodocstringstyle", search=["src"], color=True).add_args(*cli_args), + title="Checking for API breaking changes", + nofail=True, + ) + + +@duty +def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000) -> None: + """Serve the documentation (localhost:8000). + + Parameters: + host: The host to serve the docs from. + port: The port to serve the docs on. + """ + with material_insiders(): + ctx.run( + tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), + title="Serving documentation", + capture=False, + ) + + +@duty +def docs_deploy(ctx: Context) -> None: + """Deploy the documentation to GitHub pages.""" + os.environ["DEPLOY"] = "true" + with material_insiders() as insiders: + if not insiders: + ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") + origin = ctx.run("git config --get remote.origin.url", silent=True) + if "pawamoy-insiders/griffe-autodocstringstyle" in origin: + ctx.run("git remote add upstream git@github.com:mkdocstrings/griffe-autodocstringstyle", silent=True, nofail=True) + ctx.run( + tools.mkdocs.gh_deploy(remote_name="upstream", force=True), + title="Deploying documentation", + ) + else: + ctx.run( + lambda: False, + title="Not deploying docs from public repository (do that from insiders instead!)", + nofail=True, + ) + + +@duty +def format(ctx: Context) -> None: + """Run formatting tools on the code.""" + ctx.run( + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), + title="Auto-fixing code", + ) + ctx.run(tools.ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") + + +@duty +def build(ctx: Context) -> None: + """Build source and wheel distributions.""" + ctx.run( + tools.build(), + title="Building source and wheel distributions", + pty=PTY, + ) + + +@duty +def publish(ctx: Context) -> None: + """Publish source and wheel distributions to PyPI.""" + if not Path("dist").exists(): + ctx.run("false", title="No distribution files found") + dists = [str(dist) for dist in Path("dist").iterdir()] + ctx.run( + tools.twine.upload(*dists, skip_existing=True), + title="Publishing source and wheel distributions to PyPI", + pty=PTY, + ) + + +@duty(post=["build", "publish", "docs-deploy"]) +def release(ctx: Context, version: str = "") -> None: + """Release a new Python package. + + Parameters: + version: The new version number to use. + """ + origin = ctx.run("git config --get remote.origin.url", silent=True) + if "pawamoy-insiders/griffe-autodocstringstyle" in origin: + ctx.run( + lambda: False, + title="Not releasing from insiders repository (do that from public repo instead!)", + ) + if not (version := (version or input("> Version to release: ")).strip()): + ctx.run("false", title="A version must be provided") + ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) + ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) + ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) + ctx.run("git push", title="Pushing commits", pty=False) + ctx.run("git push --tags", title="Pushing tags", pty=False) + + +@duty(silent=True, aliases=["cov"]) +def coverage(ctx: Context) -> None: + """Report coverage as text and HTML.""" + ctx.run(tools.coverage.combine(), nofail=True) + ctx.run(tools.coverage.report(rcfile="config/coverage.ini"), capture=False) + ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) + + +@duty +def test(ctx: Context, *cli_args: str, match: str = "") -> None: + """Run the test suite. + + Parameters: + match: A pytest expression to filter selected tests. + """ + py_version = f"{sys.version_info.major}{sys.version_info.minor}" + os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" + ctx.run( + tools.pytest( + "tests", + config_file="config/pytest.ini", + select=match, + color="yes", + ).add_args("-n", "auto", *cli_args), + title=pyprefix("Running tests"), + ) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..35ade93 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,172 @@ +site_name: "griffe-autodocstringstyle" +site_description: "Set docstring style to 'auto' for external packages." +site_url: "https://mkdocstrings.github.io/griffe-autodocstringstyle" +repo_url: "https://github.com/mkdocstrings/griffe-autodocstringstyle" +repo_name: "mkdocstrings/griffe-autodocstringstyle" +site_dir: "site" +watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/griffe_autodocstringstyle] +copyright: Copyright © 2024 Timothée Mazzucotelli +edit_uri: edit/main/docs/ + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +nav: +- Home: + - Overview: index.md + - Changelog: changelog.md + - Credits: credits.md + - License: license.md +# defer to gen-files + literate-nav +- API reference: + - griffe-autodocstringstyle: reference/ +- Development: + - Contributing: contributing.md + - Code of Conduct: code_of_conduct.md + - Coverage report: coverage.md +- Insiders: + - insiders/index.md + - Getting started: + - Installation: insiders/installation.md + - Changelog: insiders/changelog.md +- Author's website: https://pawamoy.github.io/ + +theme: + name: material + custom_dir: docs/.overrides + icon: + logo: material/currency-sign + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.highlight + - search.suggest + - toc.follow + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: teal + accent: purple + toggle: + icon: material/weather-sunny + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: lime + toggle: + icon: material/weather-night + name: Switch to system preference + +extra_css: +- css/material.css +- css/mkdocstrings.css +- css/insiders.css + +extra_javascript: +- js/feedback.js + +markdown_extensions: +- attr_list +- admonition +- callouts +- footnotes +- pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg +- pymdownx.magiclink +- pymdownx.snippets: + base_path: [!relative $config_dir] + check_paths: true +- pymdownx.superfences +- pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower +- pymdownx.tasklist: + custom_checkbox: true +- toc: + permalink: "¤" + +plugins: +- search +- markdown-exec +- gen-files: + scripts: + - scripts/gen_ref_nav.py +- literate-nav: + nav_file: SUMMARY.md +- coverage +- mkdocstrings: + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + paths: [src] + options: + docstring_options: + ignore_init_summary: true + docstring_section_style: list + filters: ["!^_"] + heading_level: 1 + inherited_members: true + merge_init_into_class: true + separate_signature: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: true +- git-committers: + enabled: !ENV [DEPLOY, false] + repository: mkdocstrings/griffe-autodocstringstyle +- minify: + minify_html: !ENV [DEPLOY, false] +- group: + enabled: !ENV [MATERIAL_INSIDERS, false] + plugins: + - typeset + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/pawamoy + - icon: fontawesome/brands/mastodon + link: https://fosstodon.org/@pawamoy + - icon: fontawesome/brands/twitter + link: https://twitter.com/pawamoy + - icon: fontawesome/brands/gitter + link: https://gitter.im/griffe-autodocstringstyle/community + - icon: fontawesome/brands/python + link: https://pypi.org/project/griffe-autodocstringstyle/ + analytics: + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: Let us know how we can improve this page. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5b8ee12 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,66 @@ +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + +[project] +name = "griffe-autodocstringstyle" +description = "Set docstring style to 'auto' for external packages." +authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] +license = {text = "ISC"} +readme = "README.md" +requires-python = ">=3.8" +keywords = [] +dynamic = ["version"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Documentation", + "Topic :: Software Development", + "Topic :: Utilities", + "Typing :: Typed", +] +dependencies = [] + +[project.urls] +Homepage = "https://mkdocstrings.github.io/griffe-autodocstringstyle" +Documentation = "https://mkdocstrings.github.io/griffe-autodocstringstyle" +Changelog = "https://mkdocstrings.github.io/griffe-autodocstringstyle/changelog" +Repository = "https://github.com/mkdocstrings/griffe-autodocstringstyle" +Issues = "https://github.com/mkdocstrings/griffe-autodocstringstyle/issues" +Discussions = "https://github.com/mkdocstrings/griffe-autodocstringstyle/discussions" +Gitter = "https://gitter.im/mkdocstrings/griffe-autodocstringstyle" +Funding = "https://github.com/sponsors/pawamoy" + +[tool.pdm] +version = {source = "scm"} + +[tool.pdm.build] +package-dir = "src" +editable-backend = "editables" +excludes = ["**/.pytest_cache"] +source-includes = [ + "config", + "docs", + "scripts", + "share", + "tests", + "devdeps.txt", + "duties.py", + "mkdocs.yml", + "*.md", + "LICENSE", +] + +[tool.pdm.build.wheel-data] +data = [ + {path = "share/**/*", relative-to = "."}, +] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py new file mode 100644 index 0000000..af3278a --- /dev/null +++ b/scripts/gen_credits.py @@ -0,0 +1,179 @@ +"""Script to generate the project's credits.""" + +from __future__ import annotations + +import os +import sys +from collections import defaultdict +from importlib.metadata import distributions +from itertools import chain +from pathlib import Path +from textwrap import dedent +from typing import Dict, Iterable, Union + +from jinja2 import StrictUndefined +from jinja2.sandbox import SandboxedEnvironment +from packaging.requirements import Requirement + +# TODO: Remove once support for Python 3.10 is dropped. +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", ".")) +with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: + pyproject = tomllib.load(pyproject_file) +project = pyproject["project"] +project_name = project["name"] +with project_dir.joinpath("devdeps.txt").open() as devdeps_file: + devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))] + +PackageMetadata = Dict[str, Union[str, Iterable[str]]] +Metadata = Dict[str, PackageMetadata] + + +def _merge_fields(metadata: dict) -> PackageMetadata: + fields = defaultdict(list) + for header, value in metadata.items(): + fields[header.lower()].append(value.strip()) + return { + field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0] + for field, value in fields.items() + } + + +def _norm_name(name: str) -> str: + return name.replace("_", "-").replace(".", "-").lower() + + +def _requirements(deps: list[str]) -> dict[str, Requirement]: + return {_norm_name((req := Requirement(dep)).name): req for dep in deps} + + +def _extra_marker(req: Requirement) -> str | None: + if not req.marker: + return None + try: + return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra") + except StopIteration: + return None + + +def _get_metadata() -> Metadata: + metadata = {} + for pkg in distributions(): + name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore] + metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type] + metadata[name]["spec"] = set() + metadata[name]["extras"] = set() + metadata[name].setdefault("summary", "") + _set_license(metadata[name]) + return metadata + + +def _set_license(metadata: PackageMetadata) -> None: + license_field = metadata.get("license-expression", metadata.get("license", "")) + license_name = license_field if isinstance(license_field, str) else " + ".join(license_field) + check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n") + if check_classifiers: + license_names = [] + for classifier in metadata["classifier"]: + if classifier.startswith("License ::"): + license_names.append(classifier.rsplit("::", 1)[1].strip()) + license_name = " + ".join(license_names) + metadata["license"] = license_name or "?" + + +def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: + deps = {} + for dep_name, dep_req in base_deps.items(): + if dep_name not in metadata or dep_name == "griffe-autodocstringstyle": + continue + metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator] + metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator] + deps[dep_name] = metadata[dep_name] + + again = True + while again: + again = False + for pkg_name in metadata: + if pkg_name in deps: + for pkg_dependency in metadata[pkg_name].get("requires-dist", []): + requirement = Requirement(pkg_dependency) + dep_name = _norm_name(requirement.name) + extra_marker = _extra_marker(requirement) + if ( + dep_name in metadata + and dep_name not in deps + and dep_name != project["name"] + and (not extra_marker or extra_marker in deps[pkg_name]["extras"]) + ): + metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator] + deps[dep_name] = metadata[dep_name] + again = True + + return deps + + +def _render_credits() -> str: + metadata = _get_metadata() + dev_dependencies = _get_deps(_requirements(devdeps), metadata) + prod_dependencies = _get_deps( + _requirements( + chain( # type: ignore[arg-type] + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ), + ), + metadata, + ) + + template_data = { + "project_name": project_name, + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), + "more_credits": "http://pawamoy.github.io/credits/", + } + template_text = dedent( + """ + # Credits + + These projects were used to build *{{ project_name }}*. **Thank you!** + + [Python](https://www.python.org/) | + [uv](https://github.com/astral-sh/uv) | + [copier-uv](https://github.com/pawamoy/copier-uv) + + {% macro dep_line(dep) -%} + [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + {%- endmacro %} + + {% if prod_dependencies -%} + ### Runtime dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in prod_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + {% endif -%} + {% if dev_dependencies -%} + ### Development dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in dev_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + {% endif -%} + {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} + """, + ) + jinja_env = SandboxedEnvironment(undefined=StrictUndefined) + return jinja_env.from_string(template_text).render(**template_data) + + +print(_render_credits()) diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py new file mode 100644 index 0000000..6939e86 --- /dev/null +++ b/scripts/gen_ref_nav.py @@ -0,0 +1,37 @@ +"""Generate the code reference pages and navigation.""" + +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() +mod_symbol = '' + +root = Path(__file__).parent.parent +src = root / "src" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1].startswith("_"): + continue + + nav_parts = [f"{mod_symbol} {part}" for part in parts] + nav[tuple(nav_parts)] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}") + + mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/insiders.py b/scripts/insiders.py new file mode 100644 index 0000000..1521248 --- /dev/null +++ b/scripts/insiders.py @@ -0,0 +1,203 @@ +"""Functions related to Insiders funding goals.""" + +from __future__ import annotations + +import json +import logging +import os +import posixpath +from dataclasses import dataclass +from datetime import date, datetime, timedelta +from itertools import chain +from pathlib import Path +from typing import Iterable, cast +from urllib.error import HTTPError +from urllib.parse import urljoin +from urllib.request import urlopen + +import yaml + +logger = logging.getLogger(f"mkdocs.logs.{__name__}") + + +def human_readable_amount(amount: int) -> str: # noqa: D103 + str_amount = str(amount) + if len(str_amount) >= 4: # noqa: PLR2004 + return f"{str_amount[:len(str_amount)-3]},{str_amount[-3:]}" + return str_amount + + +@dataclass +class Project: + """Class representing an Insiders project.""" + + name: str + url: str + + +@dataclass +class Feature: + """Class representing an Insiders feature.""" + + name: str + ref: str | None + since: date | None + project: Project | None + + def url(self, rel_base: str = "..") -> str | None: # noqa: D102 + if not self.ref: + return None + if self.project: + rel_base = self.project.url + return posixpath.join(rel_base, self.ref.lstrip("/")) + + def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102 + new = "" + if badge: + recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011 + if recent: + ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr] + new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}' + project = f"[{self.project.name}]({self.project.url}) — " if self.project else "" + feature = f"[{self.name}]({self.url(rel_base)})" if self.ref else self.name + print(f"- [{'x' if self.since else ' '}] {project}{feature}{new}") + + +@dataclass +class Goal: + """Class representing an Insiders goal.""" + + name: str + amount: int + features: list[Feature] + complete: bool = False + + @property + def human_readable_amount(self) -> str: # noqa: D102 + return human_readable_amount(self.amount) + + def render(self, rel_base: str = "..") -> None: # noqa: D102 + print(f"#### $ {self.human_readable_amount} — {self.name}\n") + if self.features: + for feature in self.features: + feature.render(rel_base) + print("") + else: + print("There are no features in this goal for this project. ") + print( + "[See the features in this goal **for all Insiders projects.**]" + f"(https://pawamoy.github.io/insiders/#{self.amount}-{self.name.lower().replace(' ', '-')})", + ) + + +def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]: + """Load goals from JSON data. + + Parameters: + data: The JSON data. + funding: The current total funding, per month. + origin: The origin of the data (URL). + + Returns: + A dictionaries of goals, keys being their target monthly amount. + """ + goals_data = yaml.safe_load(data)["goals"] + return { + amount: Goal( + name=goal_data["name"], + amount=amount, + complete=funding >= amount, + features=[ + Feature( + name=feature_data["name"], + ref=feature_data.get("ref"), + since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 + project=project, + ) + for feature_data in goal_data["features"] + ], + ) + for amount, goal_data in goals_data.items() + } + + +def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]: + project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".") + try: + data = Path(project_dir, path).read_text() + except OSError as error: + raise RuntimeError(f"Could not load data from disk: {path}") from error + return load_goals(data, funding) + + +def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: + project_name, project_url, data_fragment = source_data + data_url = urljoin(project_url, data_fragment) + try: + with urlopen(data_url) as response: # noqa: S310 + data = response.read() + except HTTPError as error: + raise RuntimeError(f"Could not load data from network: {data_url}") from error + return load_goals(data, funding, project=Project(name=project_name, url=project_url)) + + +def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: + if isinstance(source, str): + return _load_goals_from_disk(source, funding) + return _load_goals_from_url(source, funding) + + +def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]: + """Load funding goals from a given data source. + + Parameters: + source: The data source (local file path or URL). + funding: The current total funding, per month. + + Returns: + A dictionaries of goals, keys being their target monthly amount. + """ + if isinstance(source, str): + return _load_goals_from_disk(source, funding) + goals = {} + for src in source: + source_goals = _load_goals(src, funding) + for amount, goal in source_goals.items(): + if amount not in goals: + goals[amount] = goal + else: + goals[amount].features.extend(goal.features) + return {amount: goals[amount] for amount in sorted(goals)} + + +def feature_list(goals: Iterable[Goal]) -> list[Feature]: + """Extract feature list from funding goals. + + Parameters: + goals: A list of funding goals. + + Returns: + A list of features. + """ + return list(chain.from_iterable(goal.features for goal in goals)) + + +def load_json(url: str) -> str | list | dict: # noqa: D103 + with urlopen(url) as response: # noqa: S310 + return json.loads(response.read().decode()) + + +data_source = globals()["data_source"] +sponsor_url = "https://github.com/sponsors/pawamoy" +data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main" +numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment] +sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment] +current_funding = numbers["total"] +sponsors_count = numbers["count"] +goals = funding_goals(data_source, funding=current_funding) +ongoing_goals = [goal for goal in goals.values() if not goal.complete] +unreleased_features = sorted( + (ft for ft in feature_list(ongoing_goals) if ft.since), + key=lambda ft: cast(date, ft.since), + reverse=True, +) diff --git a/scripts/make b/scripts/make new file mode 100755 index 0000000..d898022 --- /dev/null +++ b/scripts/make @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Management commands.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Iterator + +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() + +exe = "" +prefix = "" + + +def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None: + """Run a shell command.""" + if capture_output: + return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 + subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 + return None + + +@contextmanager +def environ(**kwargs: str) -> Iterator[None]: + """Temporarily set environment variables.""" + original = dict(os.environ) + os.environ.update(kwargs) + try: + yield + finally: + os.environ.clear() + os.environ.update(original) + + +def uv_install() -> None: + """Install dependencies using uv.""" + uv_opts = "" + if "UV_RESOLUTION" in os.environ: + uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" + requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) + shell("uv pip install -r -", input=requirements, text=True) + if "CI" not in os.environ: + shell("uv pip install --no-deps -e .") + else: + shell("uv pip install --no-deps .") + + +def setup() -> None: + """Setup the project.""" + if not shutil.which("uv"): + raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") + + print("Installing dependencies (default environment)") # noqa: T201 + default_venv = Path(".venv") + if not default_venv.exists(): + shell("uv venv --python python") + uv_install() + + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + print(f"\nInstalling dependencies (python{version})") # noqa: T201 + venv_path = Path(f".venvs/{version}") + if not venv_path.exists(): + shell(f"uv venv --python {version} {venv_path}") + with environ(VIRTUAL_ENV=str(venv_path.resolve())): + uv_install() + + +def activate(path: str) -> None: + """Activate a virtual environment.""" + global exe, prefix # noqa: PLW0603 + + if (bin := Path(path, "bin")).exists(): + activate_script = bin / "activate_this.py" + elif (scripts := Path(path, "Scripts")).exists(): + activate_script = scripts / "activate_this.py" + exe = ".exe" + prefix = f"{path}/Scripts/" + else: + raise ValueError(f"make: activate: Cannot find activation script in {path}") + + if not activate_script.exists(): + raise ValueError(f"make: activate: Cannot find activation script in {path}") + + exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 + + +def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in a virtual environment.""" + kwargs = {"check": True, **kwargs} + if version == "default": + activate(".venv") + subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + else: + activate(f".venvs/{version}") + os.environ["MULTIRUN"] = "1" + subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + + +def multirun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command for all configured Python versions.""" + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + run(version, cmd, *args, **kwargs) + else: + run("default", cmd, *args, **kwargs) + + +def allrun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in all virtual environments.""" + run("default", cmd, *args, **kwargs) + if PYTHON_VERSIONS: + multirun(cmd, *args, **kwargs) + + +def clean() -> None: + """Delete build artifacts and cache files.""" + paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] + for path in paths_to_clean: + shell(f"rm -rf {path}") + + cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] + for dirpath in Path(".").rglob("*"): + if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")): + shutil.rmtree(path, ignore_errors=True) + + +def vscode() -> None: + """Configure VSCode to work on this project.""" + Path(".vscode").mkdir(parents=True, exist_ok=True) + shell("cp -v config/vscode/* .vscode") + + +def main() -> int: + """Main entry point.""" + args = list(sys.argv[1:]) + if not args or args[0] == "help": + if len(args) > 1: + run("default", "duty", "--help", args[1]) + else: + print("Available commands") # noqa: T201 + print(" help Print this help. Add task name to print help.") # noqa: T201 + print(" setup Setup all virtual environments (install dependencies).") # noqa: T201 + print(" run Run a command in the default virtual environment.") # noqa: T201 + print(" multirun Run a command for all configured Python versions.") # noqa: T201 + print(" allrun Run a command in all virtual environments.") # noqa: T201 + print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201 + print(" clean Delete build artifacts and cache files.") # noqa: T201 + print(" vscode Configure VSCode to work on this project.") # noqa: T201 + try: + run("default", "python", "-V", capture_output=True) + except (subprocess.CalledProcessError, ValueError): + pass + else: + print("\nAvailable tasks") # noqa: T201 + run("default", "duty", "--list") + return 0 + + while args: + cmd = args.pop(0) + + if cmd == "run": + run("default", *args) + return 0 + + if cmd == "multirun": + multirun(*args) + return 0 + + if cmd == "allrun": + allrun(*args) + return 0 + + if cmd.startswith("3."): + run(cmd, *args) + return 0 + + opts = [] + while args and (args[0].startswith("-") or "=" in args[0]): + opts.append(args.pop(0)) + + if cmd == "clean": + clean() + elif cmd == "setup": + setup() + elif cmd == "vscode": + vscode() + elif cmd == "check": + multirun("duty", "check-quality", "check-types", "check-docs") + run("default", "duty", "check-api") + elif cmd in {"check-quality", "check-docs", "check-types", "test"}: + multirun("duty", cmd, *opts) + else: + run("default", "duty", cmd, *opts) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as process: + if process.output: + print(process.output, file=sys.stderr) # noqa: T201 + sys.exit(process.returncode) diff --git a/src/griffe_autodocstringstyle/__init__.py b/src/griffe_autodocstringstyle/__init__.py new file mode 100644 index 0000000..6dbdb3a --- /dev/null +++ b/src/griffe_autodocstringstyle/__init__.py @@ -0,0 +1,8 @@ +"""griffe-autodocstringstyle package. + +Set docstring style to 'auto' for external packages. +""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/src/griffe_autodocstringstyle/debug.py b/src/griffe_autodocstringstyle/debug.py new file mode 100644 index 0000000..e1fd30e --- /dev/null +++ b/src/griffe_autodocstringstyle/debug.py @@ -0,0 +1,109 @@ +"""Debugging utilities.""" + +from __future__ import annotations + +import os +import platform +import sys +from dataclasses import dataclass +from importlib import metadata + + +@dataclass +class Variable: + """Dataclass describing an environment variable.""" + + name: str + """Variable name.""" + value: str + """Variable value.""" + + +@dataclass +class Package: + """Dataclass describing a Python package.""" + + name: str + """Package name.""" + version: str + """Package version.""" + + +@dataclass +class Environment: + """Dataclass to store environment information.""" + + interpreter_name: str + """Python interpreter name.""" + interpreter_version: str + """Python interpreter version.""" + interpreter_path: str + """Path to Python executable.""" + platform: str + """Operating System.""" + packages: list[Package] + """Installed packages.""" + variables: list[Variable] + """Environment variables.""" + + +def _interpreter_name_version() -> tuple[str, str]: + if hasattr(sys, "implementation"): + impl = sys.implementation.version + version = f"{impl.major}.{impl.minor}.{impl.micro}" + kind = impl.releaselevel + if kind != "final": + version += kind[0] + str(impl.serial) + return sys.implementation.name, version + return "", "0.0.0" + + +def get_version(dist: str = "griffe-autodocstringstyle") -> str: + """Get version of the given distribution. + + Parameters: + dist: A distribution name. + + Returns: + A version number. + """ + try: + return metadata.version(dist) + except metadata.PackageNotFoundError: + return "0.0.0" + + +def get_debug_info() -> Environment: + """Get debug/environment information. + + Returns: + Environment information. + """ + py_name, py_version = _interpreter_name_version() + packages = ["griffe-autodocstringstyle"] + variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("GRIFFE_AUTODOCSTRINGSTYLE")]] + return Environment( + interpreter_name=py_name, + interpreter_version=py_version, + interpreter_path=sys.executable, + platform=platform.platform(), + variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], + packages=[Package(pkg, get_version(pkg)) for pkg in packages], + ) + + +def print_debug_info() -> None: + """Print debug/environment information.""" + info = get_debug_info() + print(f"- __System__: {info.platform}") + print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") + print("- __Environment variables__:") + for var in info.variables: + print(f" - `{var.name}`: `{var.value}`") + print("- __Installed packages__:") + for pkg in info.packages: + print(f" - `{pkg.name}` v{pkg.version}") + + +if __name__ == "__main__": + print_debug_info() diff --git a/src/griffe_autodocstringstyle/py.typed b/src/griffe_autodocstringstyle/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..314ba0f --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +"""Tests suite for `griffe_autodocstringstyle`.""" + +from pathlib import Path + +TESTS_DIR = Path(__file__).parent +TMP_DIR = TESTS_DIR / "tmp" +FIXTURES_DIR = TESTS_DIR / "fixtures" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3be27ba --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +"""Configuration for the pytest test suite."""