From 503ee13e470096070dc36c54cbfd9a63d1285739 Mon Sep 17 00:00:00 2001 From: Nicola VIGANO Date: Sat, 15 Jun 2024 02:41:02 +0200 Subject: [PATCH] Template update: from poetry to conda+pip Signed-off-by: Nicola VIGANO --- .github/ISSUE_TEMPLATE/bug_report.md | 70 +++-- .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.md | 20 +- .github/workflows/ci.yml | 84 +++-- .gitignore | 1 + CODE_OF_CONDUCT.md | 151 ++++++--- CONTRIBUTING.md | 94 +++--- Makefile | 59 ++-- conda/meta.yaml | 27 +- config/coverage.ini | 31 +- config/git-changelog.toml | 9 + config/mypy.ini | 2 + config/pytest.ini | 14 +- config/ruff.toml | 84 +++++ config/vscode/launch.json | 47 +++ config/vscode/settings.json | 33 ++ config/vscode/tasks.json | 97 ++++++ docs/changelog.md | 2 +- docs/code_of_conduct.md | 2 +- docs/contributing.md | 2 +- docs/credits.md | 10 + docs/css/material.css | 4 + docs/css/mkdocstrings.css | 40 ++- docs/index.md | 2 +- docs/license.md | 4 +- docs/macros.py | 64 ---- duties.py | 357 ---------------------- environment.yaml | 9 + gen_doc_stubs.py | 19 -- mkdocs.yml | 111 +++++-- pyproject.toml | 122 ++++---- scripts/gen_credits.py | 181 +++++++++++ scripts/gen_ref_nav.py | 37 +++ scripts/make | 144 +++++++++ scripts/multirun.sh | 22 -- scripts/setup.sh | 32 -- src/autoden/__init__.py | 24 +- src/autoden/__main__.py | 3 +- src/autoden/cli.py | 22 +- src/autoden/debug.py | 109 +++++++ src/autoden/py.typed | 0 tests/test_cli.py | 40 ++- 42 files changed, 1335 insertions(+), 855 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 config/git-changelog.toml create mode 100644 config/ruff.toml create mode 100644 config/vscode/launch.json create mode 100644 config/vscode/settings.json create mode 100644 config/vscode/tasks.json create mode 100644 docs/credits.md create mode 100644 docs/css/material.css delete mode 100644 docs/macros.py delete mode 100644 duties.py create mode 100644 environment.yaml delete mode 100644 gen_doc_stubs.py create mode 100644 scripts/gen_credits.py create mode 100644 scripts/gen_ref_nav.py create mode 100755 scripts/make delete mode 100644 scripts/multirun.sh delete mode 100644 scripts/setup.sh create mode 100644 src/autoden/debug.py create mode 100644 src/autoden/py.typed diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 629319a..6b9e06e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,32 +1,62 @@ --- name: Bug report -about: Create a report to help us improve -title: '' +about: Create a bug report to help us improve. +title: "bug: " labels: unconfirmed assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +### Description of the bug + -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Run command '...' -3. Scroll down to '...' -4. See error +### To Reproduce + -**System (please complete the following information):** -- `Auto-Denoise` version: [e.g. 0.2.1] -- Python version: [e.g. 3.10] -- OS: [Windows/Linux] +``` +WRITE MRE / INSTRUCTIONS HERE +``` -**Additional context** -Add any other context about the problem here. +### Full traceback + + +
Full traceback + +```python +PASTE TRACEBACK HERE +``` + +
+ +### Expected behavior + + +### Environment information + + +```bash +autoden --debug-info # | xclip -selection clipboard +``` + +PASTE OUTPUT HERE + +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3cde240 --- /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/CEA-MetroCarac/auto-denoise/discussions/new?category=q-a + about: Ask and answer questions in the Discussions tab. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4fe86d5..a57a119 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,20 @@ --- name: Feature request -about: Suggest an idea for this project -title: '' +about: Suggest an idea for this project. +title: "feature: " labels: feature assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +### Is your feature request related to a problem? Please describe. + -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +### Describe the solution you'd like + -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +### Describe alternatives you've considered + -**Additional context** -Add any other context or screenshots about the feature request here. +### Additional context + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f77703c..a1f5bc8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: ci +name: Python package on: push: @@ -16,14 +16,7 @@ env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 - - # To fix an error when running Poetry on Windows - # (https://github.com/python-poetry/poetry/issues/2629), - # we set Poetry's cache directory to .poetry_cache in the current directory. - # It makes it easier to later remove the virtualenv when it's broken. - # Absolute path is necessary to avoid this issue: - # https://github.com/python-poetry/poetry/issues/3049 - POETRY_CACHE_DIR: ${{ github.workspace }}/.poetry_cache + PYTHON_VERSIONS: "" jobs: @@ -35,42 +28,41 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Fetch all tags + run: git fetch --depth=1 --tags + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - - name: Set up Poetry - run: pip install poetry - - - name: Set up the cache - uses: actions/cache@v3 - with: - path: .poetry_cache - key: quality-poetry-cache - - - name: Set up the project - run: poetry install -vv - - - name: Check if the documentation builds correctly - run: poetry run duty check-docs - - - name: Check the code quality - run: poetry run duty check-code-quality + - name: Python info + shell: bash -e {0} + run: | + which python + python --version - - name: Check if the code is correctly typed - run: poetry run duty check-types + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools build + python -m pip install flake8 pytest + python -m pip install . - - name: Check for vulnerabilities in dependencies + - name: Lint with flake8 run: | - pip install safety - poetry run duty check-dependencies + # stop the build if there are Python syntax errors or undefined names + flake8 src/autoden tests examples --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 src/autoden tests examples --count --exit-zero --max-complexity=15 --max-line-length=127 --statistics tests: strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: + - ubuntu-latest + # - windows-latest + # - macos-latest python-version: [ "3.10", "3.11", "3.12" ] runs-on: ${{ matrix.os }} @@ -80,21 +72,21 @@ jobs: uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - - name: Set up Poetry - run: pip install poetry - - - name: Set up the cache - uses: actions/cache@v3 - with: - path: .poetry_cache - key: tests-poetry-cache-${{ matrix.os }}-py${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools build + python -m pip install flake8 pytest + python -m pip install . - - name: Set up the project - run: poetry install -vv || { rm -rf .poetry_cache/virtualenvs/*; poetry install -vv; } + - name: Test with pytest + run: | + pytest tests/ - - name: Run the test suite - run: poetry run duty test + - name: Verify that we can build the package + run: | + python -m build \ No newline at end of file diff --git a/.gitignore b/.gitignore index 68bc17f..ac9d074 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,7 @@ venv.bak/ # mypy .mypy_cache/ +.ruff_cache/ .dmypy.json dmypy.json diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 712c5cb..898f5f7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,73 +2,132 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +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 creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* 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 by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* 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 electronic - address, without explicit permission +* 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 -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +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. -Project maintainers 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, or to ban temporarily or -permanently any contributor for other behaviors 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 both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +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 by contacting the project team at nicola.vigano@cea.fr. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +nicola.vigano@cea.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. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**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 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +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 -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7093e9..9674ee0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,24 +14,26 @@ cd auto-denoise make setup ``` -!!! note - If it fails for some reason, - you'll need to install - [Poetry](https://github.com/python-poetry/poetry) - manually. - - You can install it with: - - ```bash - python3 -m pip install --user pipx - pipx install poetry - ``` - - Now you can try running `make setup` again, - or simply `poetry install`. +> 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. +You can run the application with `make run autoden [ARGS...]`. + Run `make help` to see all the available actions! ## Tasks @@ -39,35 +41,30 @@ Run `make help` to see all the available actions! This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task -on multiple Python versions, you can do one of the following: - -1. `export PYTHON_VERSIONS= `: this will run the task - with only the current Python version -2. run the task directly with `poetry run duty TASK`, - or `duty TASK` if the environment was already activated - through `poetry shell` +on multiple Python versions, you run the task directly with `make run duty TASK`. -The Makefile detects if the Poetry environment is activated, +The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. +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 checkout -b feature-or-bugfix-name` +1. create a new branch: `git switch -c feature-or-bugfix-name` 1. edit the code and/or the documentation -If you updated the documentation or the project dependencies: - -1. run `make docs-regen` -1. run `make docs-serve`, - go to http://localhost:8000 and check that everything looks good - **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, @@ -78,8 +75,9 @@ Don't bother updating the changelog, we will take care of this. ## Commit message convention -Commits messages must follow the -[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message): +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 @@ -87,34 +85,52 @@ Commits messages must follow the [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 which are not features nor bug fixes. +- `refactor`: Changes that are not features or bug fixes. - `style`: A change in code style/format. - `tests`: About tests. -**Subject (and body) must be valid Markdown.** -If you write a body, please add issues references at the end: +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. -References: #10, #11. -Fixes #15. +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 review, we recommend using fixups: +During the review, we recommend using fixups: ```bash # SHA is the SHA of the commit you want to fix @@ -124,7 +140,7 @@ git commit --fixup=SHA Once all the changes are approved, you can squash your commits: ```bash -git rebase -i --autosquash master +git rebase -i --autosquash main ``` And force-push: diff --git a/Makefile b/Makefile index 56c7b5f..5e88121 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,28 @@ -.DEFAULT_GOAL := help -SHELL := bash +# 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. -DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo poetry run) duty - -args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) -check_code_quality_args = files -docs_serve_args = host port -release_args = version -test_args = match - -BASIC_DUTIES = \ +actions = \ + allrun \ changelog \ + check \ + check-api \ + check-docs \ + check-quality \ + check-types \ clean \ coverage \ docs \ docs-deploy \ - docs-regen \ - docs-serve \ format \ - release - -QUALITY_DUTIES = \ - check \ - check-code-quality \ - check-dependencies \ - check-docs \ - check-types \ - test - -.PHONY: help -help: - @$(DUTY) --list - -.PHONY: setup -setup: - @bash scripts/setup.sh - -.PHONY: $(BASIC_DUTIES) -$(BASIC_DUTIES): - @$(DUTY) $@ $(call args,$@) - -.PHONY: $(QUALITY_DUTIES) -$(QUALITY_DUTIES): - @bash scripts/multirun.sh duty $@ $(call args,$@) + help \ + multirun \ + release \ + run \ + setup \ + test \ + vscode + +.PHONY: $(actions) +$(actions): + @python scripts/make "$@" diff --git a/conda/meta.yaml b/conda/meta.yaml index 48d7d26..b0f5a4d 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -1,10 +1,11 @@ -{% set pyproject = load_file_data('pyproject.toml') %} -{% set poetry = pyproject.get('tool', {}).get('poetry') %} +{%- set pyproject = load_file_data('pyproject.toml') %} +{%- set project = pyproject.get('project', {}) %} +{%- set env_file = load_file_data('environment.yaml') %} package: - name: {{ poetry.get('name') }} - version: {{ poetry.get('version') }} + name: {{ project['name'] }} + version: {{ project['version'] }} source: path: ../ @@ -22,17 +23,21 @@ requirements: run: - python - # dependencies are defined in pyproject.toml - {% for dep in poetry.get('dependencies') %} - - {{ dep.lower() }} + # Dependencies as defined in environment.yaml + # Some names might be different from pypi, so please curate that list manually + {% for dep in env_file['dependencies'] %} + - {{ dep }} {% endfor %} about: - home: {{ poetry.get('repository') }} + home: {{ project['urls']['Homepage'] }} license_file: LICENSE - summary: {{ poetry.get('description') }} - doc_url: {{ poetry.get('documentation') }} + summary: {{ project['description'] }} + doc_url: {{ project['urls']['Documentation'] }} extra: maintainers: - - {{ poetry.get('authors') }} + {% for author in project['authors'] %} + - "{{ author.name }} <{{ author.email }}>" + {% endfor %} + diff --git a/config/coverage.ini b/config/coverage.ini index a996004..b56a286 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -1,24 +1,25 @@ -[coverage:paths] -source = - src/autoden - */site-packages/autoden - [coverage:run] branch = true -source = - src/autoden - tests parallel = true +source = + src/ + tests/ + +[coverage:paths] +equivalent = + src/ + .venv/lib/*/site-packages/ + .venvs/*/lib/*/site-packages/ [coverage:report] precision = 2 omit = - src/autoden/__init__.py - src/autoden/__main__.py - tests/* - -[coverage:html] -directory = build/coverage + src/*/__init__.py + src/*/__main__.py + tests/__init__.py +exclude_lines = + pragma: no cover + if TYPE_CHECKING [coverage:json] -output = build/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 index 98efbae..814e2ac 100644 --- a/config/mypy.ini +++ b/config/mypy.ini @@ -1,3 +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 index ad72bbe..052a2f1 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -1,16 +1,14 @@ [pytest] -norecursedirs = - .git - .tox - .env - dist - build python_files = test_*.py - *_test.py - tests.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..5c902d3 --- /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 = ["autoden"] + +[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/docs/changelog.md b/docs/changelog.md index 90cb31c..786b75d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1 +1 @@ ---8<-- "CHANGELOG.md" \ No newline at end of file +--8<-- "CHANGELOG.md" diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md index c2cf022..01f2ea2 100644 --- a/docs/code_of_conduct.md +++ b/docs/code_of_conduct.md @@ -1 +1 @@ ---8<-- "CODE_OF_CONDUCT.md" \ No newline at end of file +--8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md index e079654..ea38c9b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1 +1 @@ ---8<-- "CONTRIBUTING.md" \ No newline at end of file +--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/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 index 54950fb..88c7357 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -1,33 +1,27 @@ /* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; - border-left: 4px solid rgba(230, 230, 230); - margin-bottom: 80px; + border-left: .05rem solid var(--md-typeset-table-color); } -/* Don't capitalize names. */ -h5.doc-heading { - text-transform: none !important; -} - -/* Don't use vertical space on hidden ToC entries. */ -.hidden-toc::before { - margin-top: 0 !important; - padding-top: 0 !important; -} +/* 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: ' '; -/* Don't show permalink of hidden ToC entries. */ -.hidden-toc a.headerlink { - display: none; -} + display: inline-block; + vertical-align: middle; + position: relative; -/* Avoid breaking parameters name, etc. in table cells. */ -td code { - word-break: normal !important; + height: 1em; + width: 1em; + background-color: currentColor; } -/* For pieces of Markdown rendered in table cells. */ -td p { - margin-top: 0 !important; - margin-bottom: 0 !important; +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 index 4ab0748..612c7a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1 @@ ---8<-- "README.md" \ No newline at end of file +--8<-- "README.md" diff --git a/docs/license.md b/docs/license.md index e34968b..a873d2b 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,3 +1,5 @@ +# License + ``` --8<-- "LICENSE" -``` \ No newline at end of file +``` diff --git a/docs/macros.py b/docs/macros.py deleted file mode 100644 index e279cdb..0000000 --- a/docs/macros.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Macros and filters made available in Markdown pages.""" - -from itertools import chain -from pathlib import Path - -import toml -from pip._internal.commands.show import search_packages_info # noqa: WPS436 (no other way?) - - -def get_credits_data() -> dict: - """ - Return data used to generate the credits file. - - Returns: - Data required to render the credits template. - """ - project_dir = Path(__file__).parent.parent - metadata = toml.load(project_dir / "pyproject.toml")["tool"]["poetry"] - lock_data = toml.load(project_dir / "poetry.lock") - project_name = metadata["name"] - - direct_dependencies = set(metadata["dependencies"].keys()) - direct_dependencies.remove("python") - dev_dependencies = set(metadata["dev-dependencies"].keys()) - poetry_dependencies = set(chain(direct_dependencies, dev_dependencies)) - indirect_dependencies = { - pkg["name"].lower() for pkg in lock_data["package"] if pkg["name"].lower() not in poetry_dependencies - } - - packages = {} - all_pkgs = poetry_dependencies.copy() - all_pkgs.update(indirect_dependencies) - for pkg in search_packages_info(list(all_pkgs)): - # NOTE walrus can be used - name = pkg.name - if name: - packages[name.lower()] = {key: getattr(pkg, key) for key in dir(pkg) if not key.startswith("_")} - - # all packages might not be credited, - # like the ones that are now part of the standard library - # or the ones that are only used on other operating systems, - # and therefore are not installed, - # but it's not that important - - return { - "project_name": project_name, - "direct_dependencies": sorted(direct_dependencies), - "dev_dependencies": sorted(dev_dependencies), - "indirect_dependencies": sorted(indirect_dependencies), - "package_info": packages, - } - - -def define_env(env): - """ - Add macros and filters into the Jinja2 environment. - - This hook is called by `mkdocs-macros-plugin` - when building the documentation. - - Arguments: - env: An object used to add macros and filters to the environment. - """ - env.macro(get_credits_data, "get_credits_data") diff --git a/duties.py b/duties.py deleted file mode 100644 index 0f21f31..0000000 --- a/duties.py +++ /dev/null @@ -1,357 +0,0 @@ -"""Development tasks.""" - -import inspect -import os -import re -import sys -from pathlib import Path -from shutil import which -from typing import List, Optional, Pattern - -import httpx -from duty import duty -from git_changelog.build import Changelog, Version -from jinja2.sandbox import SandboxedEnvironment - -PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "docs/macros.py")) -PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) -PY_SRC = " ".join(PY_SRC_LIST) -TESTING = os.environ.get("TESTING", "0") in {"1", "true"} -CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} -WINDOWS = os.name == "nt" -PTY = not WINDOWS and not CI - - -def latest(lines: List[str], regex: Pattern) -> Optional[str]: - """ - Return the last released version. - - Arguments: - lines: Lines of the changelog file. - regex: A compiled regex to find version numbers. - - Returns: - The last version. - """ - for line in lines: - match = regex.search(line) - if match: - return match.groupdict()["version"] - return None - - -def unreleased(versions: List[Version], last_release: str) -> List[Version]: - """ - Return the most recent versions down to latest release. - - Arguments: - versions: All the versions (released and unreleased). - last_release: The latest release. - - Returns: - A list of versions. - """ - for index, version in enumerate(versions): - if version.tag == last_release: - return versions[:index] - return versions - - -def read_changelog(filepath: str) -> List[str]: - """ - Read the changelog file. - - Arguments: - filepath: The path to the changelog file. - - Returns: - The changelog lines. - """ - with open(filepath, "r") as changelog_file: - return changelog_file.read().splitlines() - - -def write_changelog(filepath: str, lines: List[str]) -> None: - """ - Write the changelog file. - - Arguments: - filepath: The path to the changelog file. - lines: The lines to write to the file. - """ - with open(filepath, "w") as changelog_file: - changelog_file.write("\n".join(lines).rstrip("\n") + "\n") - - -def update_changelog( - inplace_file: str, - marker: str, - version_regex: str, - template_url: str, - commit_style: str, -) -> None: - """ - Update the given changelog file in place. - - Arguments: - inplace_file: The file to update in-place. - marker: The line after which to insert new contents. - version_regex: A regular expression to find currently documented versions in the file. - template_url: The URL to the Jinja template used to render contents. - commit_style: The style of commit messages to parse. - """ - env = SandboxedEnvironment(autoescape=False) - template = env.from_string(httpx.get(template_url).text) - changelog = Changelog(".", style=commit_style) - - if len(changelog.versions_list) == 1: - last_version = changelog.versions_list[0] - if last_version.planned_tag is None: - planned_tag = "0.1.0" - last_version.tag = planned_tag - last_version.url += planned_tag - last_version.compare_url = last_version.compare_url.replace("HEAD", planned_tag) - - lines = read_changelog(inplace_file) - last_released = latest(lines, re.compile(version_regex)) - if last_released: - changelog.versions_list = unreleased(changelog.versions_list, last_released) - rendered = template.render(changelog=changelog, inplace=True) - lines[lines.index(marker)] = rendered - write_changelog(inplace_file, lines) - - -@duty -def changelog(ctx): - """ - Update the changelog in-place with latest commits. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run( - update_changelog, - kwargs={ - "inplace_file": "CHANGELOG.md", - "marker": "", - "version_regex": r"^## \[v?(?P[^\}}+)", - "template_url": "https://raw.githubusercontent.com/pawamoy/jinja-templates/master/keepachangelog.md", - "commit_style": "angular", - }, - title="Updating changelog", - pty=PTY, - ) - - -@duty(pre=["check_code_quality", "check_types", "check_docs", "check_dependencies"]) -def check(ctx): - """ - Check it all! - - Arguments: - ctx: The context instance (passed automatically). - """ - - -@duty -def check_code_quality(ctx, files=PY_SRC): - """ - Check the code quality. - - Arguments: - ctx: The context instance (passed automatically). - files: The files to check. - """ - ctx.run(f"flake8 --config=config/flake8.ini {files}", title="Checking code quality", pty=PTY) - - -@duty -def check_dependencies(ctx): - """ - Check for vulnerabilities in dependencies. - - Arguments: - ctx: The context instance (passed automatically). - """ - nofail = False - safety = which("safety") - if not safety: - pipx = which("pipx") - if pipx: - safety = f"{pipx} run safety" - else: - - def safety_not_available(): # noqa: WPS430 - print( - inspect.cleandoc( # noqa: WPS462 - """ - Please install safety or pipx to run this task: - - pip install --user safety - # or - pip install --user pipx - # and potentially - pipx install safety - - See https://github.com/advisories/GHSA-7q25-qrjw-6fg2 - """ - ) # noqa: WPS355 - ) - return 1 - - ctx.run(safety_not_available, title="Checking dependencies", nofail=True) - return - ctx.run( - f"poetry export -f requirements.txt --without-hashes | {safety} check --stdin --full-report", - title="Checking dependencies", - pty=PTY, - nofail=nofail, - ) - - -@duty -def check_docs(ctx): - """ - Check if the documentation builds correctly. - - Arguments: - ctx: The context instance (passed automatically). - """ - Path("build/coverage").mkdir(parents=True, exist_ok=True) - Path("build/coverage/index.html").touch(exist_ok=True) - ctx.run("mkdocs build -s", title="Building documentation", pty=PTY) - - -@duty -def check_types(ctx): - """ - Check that the code is correctly typed. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run(f"mypy --config-file config/mypy.ini {PY_SRC}", title="Type-checking", pty=PTY) - - -@duty(silent=True) -def clean(ctx): - """ - Delete temporary files. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run("rm -rf .coverage*") - ctx.run("rm -rf .mypy_cache") - ctx.run("rm -rf .pytest_cache") - ctx.run("rm -rf tests/.pytest_cache") - ctx.run("rm -rf build") - ctx.run("rm -rf dist") - ctx.run("rm -rf pip-wheel-metadata") - ctx.run("rm -rf site") - ctx.run("find . -type d -name __pycache__ | xargs rm -rf") - ctx.run("find . -name '*.rej' -delete") - - -@duty -def docs(ctx): - """ - Build the documentation locally. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run("mkdocs build", title="Building documentation") - - -@duty -def docs_serve(ctx, host="127.0.0.1", port=8000): - """ - Serve the documentation (localhost:8000). - - Arguments: - ctx: The context instance (passed automatically). - host: The host to serve the docs from. - port: The port to serve the docs on. - """ - ctx.run(f"mkdocs serve -a {host}:{port}", title="Serving documentation", capture=False) - - -@duty -def docs_deploy(ctx): - """ - Deploy the documentation on GitHub pages. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run("mkdocs gh-deploy", title="Deploying documentation") - - -@duty -def format(ctx): - """ - Run formatting tools on the code. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run( - f"autoflake -ir --exclude tests/fixtures --remove-all-unused-imports {PY_SRC}", - title="Removing unused imports", - pty=PTY, - ) - ctx.run(f"isort {PY_SRC}", title="Ordering imports", pty=PTY) - ctx.run(f"black {PY_SRC}", title="Formatting code", pty=PTY) - - -@duty -def release(ctx, version): - """ - Release a new Python package. - - Arguments: - ctx: The context instance (passed automatically). - version: The new version number to use. - """ - ctx.run(f"poetry version {version}", title=f"Bumping version in pyproject.toml to {version}", pty=PTY) - 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) - if not TESTING: - ctx.run("git push", title="Pushing commits", pty=False) - ctx.run("git push --tags", title="Pushing tags", pty=False) - ctx.run("poetry build", title="Building dist/wheel", pty=PTY) - ctx.run("poetry publish", title="Publishing version", pty=PTY) - docs_deploy.run() # type: ignore - - -@duty(silent=True) -def coverage(ctx): - """ - Report coverage as text and HTML. - - Arguments: - ctx: The context instance (passed automatically). - """ - ctx.run("coverage combine .coverage-*", nofail=True) - ctx.run("coverage report --rcfile=config/coverage.ini", capture=False) - ctx.run("coverage html --rcfile=config/coverage.ini") - - -@duty -def test(ctx, match: str = ""): - """ - Run the test suite. - - Arguments: - ctx: The context instance (passed automatically). - 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( - ["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"], - title="Running tests", - pty=PTY, - ) diff --git a/environment.yaml b/environment.yaml new file mode 100644 index 0000000..c17970a --- /dev/null +++ b/environment.yaml @@ -0,0 +1,9 @@ +dependencies: + - python + - numpy>=1.21 + - scipy + - tqdm + - matplotlib + - imageio + - scikit-image + - pytorch \ No newline at end of file diff --git a/gen_doc_stubs.py b/gen_doc_stubs.py deleted file mode 100644 index db73a2c..0000000 --- a/gen_doc_stubs.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Automatic documentation generation. -""" - -from pathlib import Path - -import mkdocs_gen_files - -src_root = Path("src/autoden") -for path in src_root.glob("**/*.py"): - doc_path = Path("reference", path.relative_to(src_root)).with_suffix(".md") - if path.parts[-1][:2] == "__": - print(f"Excluding documentation page for: {doc_path}") - continue - print(f"Generating documentation page for: {doc_path}") - - with mkdocs_gen_files.open(doc_path, "w") as f: - ident = ".".join(path.with_suffix("").parts[1:]) - print("::: " + ident, file=f) diff --git a/mkdocs.yml b/mkdocs.yml index f6fbc61..06384f9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,22 +4,24 @@ site_url: "https://CEA-MetroCarac.github.io/auto-denoise" repo_url: "https://github.com/CEA-MetroCarac/auto-denoise" repo_name: "CEA-MetroCarac/auto-denoise" site_dir: "site" +watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/autoden] +copyright: Copyright © 2023 Nicola VIGANO +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 -- Code Reference: - - algorithms.py: reference/algorithms.md - - datasets.py: reference/datasets.md - - losses.py: reference/losses.md - - models: - - config.py: reference/models/config.md - - dncnn.py: reference/models/dncnn.md - - msd.py: reference/models/msd.md - - unet.py: reference/models/unet.md - - cli.py: reference/cli.md +# defer to gen-files + literate-nav +- API reference: + - Auto-Denoise: reference/ - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md @@ -27,42 +29,109 @@ nav: theme: name: material 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: - - scheme: blue + - 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/brightness-7 + icon: material/weather-sunny name: Switch to dark mode - - scheme: slate + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: lime toggle: - icon: material/brightness-4 - name: Switch to light mode + icon: material/weather-night + name: Switch to system preference extra_css: +- css/material.css - css/mkdocstrings.css markdown_extensions: +- attr_list - admonition -- pymdownx.emoji +- 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 -- pymdownx.tasklist +- pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower +- pymdownx.tasklist: + custom_checkbox: true - toc: permalink: true plugins: - search - autorefs +- literate-nav: + nav_file: SUMMARY.md - mkdocstrings: handlers: python: + import: + - https://docs.python.org/3/objects.inv + paths: [src] options: docstring_style: numpy -- macros: - module_name: docs/macros + 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 - gen-files: scripts: - - gen_doc_stubs.py + - scripts/gen_ref_nav.py +- git-committers: + enabled: !ENV [DEPLOY, false] + repository: CEA-MetroCarac/auto-denoise +- group: + enabled: !ENV [MATERIAL_INSIDERS, false] + plugins: + - typeset + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Obi-Wan + - icon: fontawesome/brands/python + link: https://pypi.org/project/auto-denoise/ diff --git a/pyproject.toml b/pyproject.toml index 05e7890..4426246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,70 +1,84 @@ [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["setuptools"] +build-backend = "setuptools.build_meta" -[tool.poetry] +[project] name = "auto-denoise" version = "0.1.0" description = "Unsupervised and self-supervised CNN denoising methods." -authors = ["Nicola VIGANO "] -license = "MIT License" -readme = "README.md" -repository = "https://github.com/CEA-MetroCarac/auto-denoise" -homepage = "https://github.com/CEA-MetroCarac/auto-denoise" -documentation = "https://CEA-MetroCarac.github.com.io/auto-denoise/" +authors = [{name = "Nicola VIGANO", email = "nicola.vigano@cea.fr"}] +license = {text = "MIT"} +readme = {file = "README.md", content-type = "text/markdown"} +requires-python = ">=3.10" keywords = [] -packages = [ { include = "autoden", from = "src" } ] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Documentation", + "Topic :: Software Development", + "Topic :: Utilities", + "Typing :: Typed", +] +dependencies = [ + "python>=3.10", + "numpy>=1.21", + "scipy", + "tqdm", + "matplotlib", + "imageio", + "scikit-image", + "torch", +] -[tool.poetry.dependencies] -python = ">=3.10" -numpy = ">=1.21" -scipy = "*" -tqdm = "*" -matplotlib = "*" -imageio = "*" -scikit-image = "*" +[project.optional-dependencies] +dev = [ + # dev + "editables>=0.5", -[tool.poetry.extras] -scikit-image = ["data"] + # maintenance + "build>=1.2", + "git-changelog>=2.4", + "twine>=5.1; python_version < '3.13'", -[tool.poetry.dev-dependencies] -# formatting, quality, tests -autoflake = ">=1.4" -black = "*" -isort = "*" -pytest = "*" -types-toml = ">=0.10.1" + # ci + "isort", + "pyupgrade", + "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", + "flake8", -# tasks -duty = ">=0.6.0" -git-changelog = ">=0.5.0" -httpx = ">=0.16.1" -jinja2-cli = ">=0.7.0" -toml = ">=0.10.2" + # docs + "black>=24.4", + "mkdocs>=1.6", + "mkdocs-gen-files>=0.4", + "mkdocs-git-committers-plugin-2>=2.3", + "mkdocs-literate-nav>=0.6", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.25", + "tomli>=2.0; python_version < '3.11'", +] -# flake8 plugins -flake8 = ">=3.7.0" -flake8-bandit = ">=2.1.2" -flake8-black = ">=0.2.1" -flake8-bugbear = ">=20.11.1" -flake8-builtins = ">=1.5.3" -flake8-comprehensions = ">=3.3.1" -flake8-docstrings = ">=1.5.0" -flake8-string-format = ">=0.3.0" -flake8-tidy-imports = ">=4.2.1" -flake8-variables-names = ">=0.0.4" -pep8-naming = ">=0.11.1" -pydocstyle = ">=6.1.1" +[project.urls] +Homepage = "https://CEA-MetroCarac.github.io/auto-denoise" +Documentation = "https://CEA-MetroCarac.github.io/auto-denoise" +Changelog = "https://CEA-MetroCarac.github.io/auto-denoise/changelog" +Repository = "https://github.com/CEA-MetroCarac/auto-denoise" +Issues = "https://github.com/CEA-MetroCarac/auto-denoise/issues" +Discussions = "https://github.com/CEA-MetroCarac/auto-denoise/discussions" -# docs -mkdocs = ">=1.2.2" -mkdocs-macros-plugin = ">=0.5.0" -mkdocs-material = ">=6.2.7" -mkdocstrings = ">=0.16.2" - -[tool.poetry.scripts] +[project.scripts] autoden = "autoden.cli:main" - [tool.black] line-length = 127 exclude = "tests/fixtures" diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py new file mode 100644 index 0000000..0ec7dd4 --- /dev/null +++ b/scripts/gen_credits.py @@ -0,0 +1,181 @@ +"""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"] +devdeps = [line.strip() for line in project["optional-dependencies"]["dev"]] +devdeps = [line for line in devdeps if line and not line.startswith(("-e", "#")) and line != ""] +# with project_dir.joinpath("requirements-dev.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 == "auto-denoise": + 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": "", + } + 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/make b/scripts/make new file mode 100755 index 0000000..e0fcb80 --- /dev/null +++ b/scripts/make @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Management commands.""" + +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import yaml + +try: + # TODO: Remove once support for Python 3.10 is dropped. + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib +except ImportError: + print("Could not import tomli/tomlib, please install the package 'build', and then retry") + + +def shell(cmd: str) -> None: + """Run a shell command.""" + print(f"Calling: `{cmd}`") + subprocess.run(cmd, shell=True, check=True) # noqa: S602 + + +def is_pkg_installed(pkg: str) -> bool: + """Checks whether a conda package is installed. + + Parameters + ---------- + pkg : str + Package name + + Returns + ------- + bool + Whether it is installed. + """ + print(f" * Checking that {pkg} is present..", flush=False, end="") + pkg = pkg.split("==")[0] + pkg = pkg.split("<=")[0] + pkg = pkg.split(">=")[0] + pkg = pkg.split("<")[0] + pkg = pkg.split(">")[0] + result = subprocess.run(f"conda list --full-name {pkg} -e -c", shell=True, check=True, capture_output=True, text=True) + is_present = result.stdout.strip() != "" + if is_present: + print("\b\b: Found!") + else: + print("\b\b: Not found!") + return is_present + + +def _process_dep(dep: str) -> str: + mtch = re.match("(?P\w+)\[(?P