pax_global_header00006660000000000000000000000064150012362540014510gustar00rootroot0000000000000052 comment=71d44dd31b86a3d15be0b916c6b70083636db4c7 scriv-1.7.0/000077500000000000000000000000001500123625400126435ustar00rootroot00000000000000scriv-1.7.0/.editorconfig000066400000000000000000000007551500123625400153270ustar00rootroot00000000000000[*] end_of_line = lf insert_final_newline = true charset = utf-8 indent_style = space indent_size = 4 max_line_length = 80 trim_trailing_whitespace = true [{Makefile, *.mk}] indent_style = tab indent_size = 8 [*.{yml,yaml,json}] indent_size = 2 [*.js] indent_size = 2 [*.diff] trim_trailing_whitespace = false [.git/*] trim_trailing_whitespace = false [*.rst] max_line_length = 79 [requirements/*.{in,txt}] # No idea why, but pip-tools puts comments on 2-space indents. indent_size = 2 scriv-1.7.0/.github/000077500000000000000000000000001500123625400142035ustar00rootroot00000000000000scriv-1.7.0/.github/FUNDING.yml000066400000000000000000000000171500123625400160160ustar00rootroot00000000000000github: nedbat scriv-1.7.0/.github/dependabot.yml000066400000000000000000000007641500123625400170420ustar00rootroot00000000000000# From: # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/keeping-your-actions-up-to-date-with-dependabot # Set update schedule for GitHub Actions version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions once a week interval: "weekly" groups: action-dependencies: patterns: - "*" commit-message: prefix: "chore" scriv-1.7.0/.github/workflows/000077500000000000000000000000001500123625400162405ustar00rootroot00000000000000scriv-1.7.0/.github/workflows/tests.yml000066400000000000000000000137641500123625400201400ustar00rootroot00000000000000# Run scriv CI name: "Test Suite" on: push: pull_request: workflow_dispatch: permissions: contents: read defaults: run: shell: bash concurrency: group: "${{ github.workflow }}-${{ github.ref }}" cancel-in-progress: true env: PIP_DISABLE_PIP_VERSION_CHECK: 1 PANDOC_VER: 3.6.3 jobs: tests: name: "Test on ${{ matrix.os }}" runs-on: "${{ matrix.os }}-latest" strategy: fail-fast: false matrix: os: - ubuntu - macos - windows steps: - name: "Check out the repo" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Set up Python" id: "setup-python" uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: # The last listed Python version is the default. python-version: | pypy-3.9 3.9 3.10 3.11 3.12 3.13 - name: "Restore cache" id: "restore-cache" uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: | .tox/ .venv/ key: "cache-python-${{ steps.setup-python.outputs.python-version }}-os-${{ runner.os }}-hash-${{ hashFiles('tox.ini', 'requirements/*.txt') }}" - name: "Identify venv path" shell: "bash" env: VENV_PATH: ${{ runner.os == 'Windows' && '.venv/Scripts' || '.venv/bin' }} run: | echo "venv_path=${VENV_PATH}" >> $GITHUB_ENV - name: "Install dependencies" if: "steps.restore-cache.outputs.cache-hit == false" run: | python -m venv .venv ${venv_path}/python -m pip install -U setuptools ${venv_path}/python -m pip install -r requirements/tox.txt - name: "Install pandoc on Linux" # sudo apt-get pandoc: will install a version from 2018! if: runner.os == 'Linux' run: | wget -nv -O pandoc.deb https://github.com/jgm/pandoc/releases/download/${PANDOC_VER}/pandoc-${PANDOC_VER}-1-amd64.deb sudo apt install ./pandoc.deb - name: "Install pandoc on Mac" if: runner.os == 'macOS' run: | brew install pandoc - name: "Install pandoc on Windows" if: runner.os == 'Windows' run: | choco install -y -r --no-progress pandoc - name: "Run tox" run: | ${venv_path}/python -m tox -m ci-tests - name: "Upload coverage data" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: covdata-${{ matrix.os }} path: .coverage.* include-hidden-files: true coverage: name: Coverage needs: tests runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Set up Python" uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: "3.12" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Download coverage data" uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: pattern: covdata-* merge-multiple: true - name: "Combine and report" run: | python -m tox -e coverage export TOTAL=$(python -c "import json;print(json.load(open('coverage.json'))['totals']['percent_covered_display'])") echo "total=$TOTAL" >> $GITHUB_ENV echo "### Total coverage: ${TOTAL}%" >> $GITHUB_STEP_SUMMARY - name: "Make badge" if: (github.repository == 'nedbat/scriv') && (github.ref == 'refs/heads/main') # https://gist.github.com/nedbat/5a304c1c779d4bcc57be95f847e9327f uses: schneegans/dynamic-badges-action@e9a478b16159b4d31420099ba146cdc50f134483 # v1.7.0 with: # GIST_TOKEN is a GitHub personal access token with scope "gist". # https://github.com/settings/tokens/969369418 auth: ${{ secrets.GIST_TOKEN }} gistID: 5a304c1c779d4bcc57be95f847e9327f filename: covbadge.json label: Coverage message: ${{ env.total }}% minColorRange: 50 maxColorRange: 90 valColorRange: ${{ env.total }} docs: name: Docs runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Set up Python" uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: "3.9" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Build docs" run: | python -m tox -e docs quality: name: Linters etc runs-on: ubuntu-latest steps: - name: "Check out the repo" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Set up Python" uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0 with: python-version: "3.9" cache: pip cache-dependency-path: 'requirements/*.txt' - name: "Install dependencies" run: | python -m pip install -U setuptools python -m pip install -r requirements/tox.txt - name: "Linters etc" run: | python -m tox -e quality scriv-1.7.0/.github/zizmor.yml000066400000000000000000000002251500123625400162570ustar00rootroot00000000000000# Rules for checking workflows # https://woodruffw.github.io/zizmor rules: unpinned-uses: config: policies: actions/*: hash-pin scriv-1.7.0/.gitignore000066400000000000000000000012001500123625400146240ustar00rootroot00000000000000*.py[cod] __pycache__ .pytest_cache .mypy_cache # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .cache/ .pytest_cache/ .coverage .coverage.* .tox coverage.json coverage.xml htmlcov/ # IDEs and text editors *~ *.swp .idea/ .project .pycharm_helpers/ .pydevproject # The Silver Searcher .agignore # OS X artifacts *.DS_Store # Logging log/ logs/ chromedriver.log ghostdriver.log # Complexity output/*.html output/*/index.html # Sphinx docs/_build docs/modules.rst docs/journo.rst docs/journo.*.rst scriv-1.7.0/.ignore000066400000000000000000000002041500123625400141230ustar00rootroot00000000000000# .ignore: control what files get searched. build htmlcov .tox* .coverage* _build _spell *.egg *.egg-info .mypy_cache .pytest_cache scriv-1.7.0/.mailmap000066400000000000000000000002131500123625400142600ustar00rootroot00000000000000# https://git-scm.com/docs/gitmailmap # This file isn't supported by GitHub (yet?) but maybe someday scriv-1.7.0/.readthedocs.yaml000066400000000000000000000005321500123625400160720ustar00rootroot00000000000000# ReadTheDocs configuration. # See https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: builder: html configuration: docs/conf.py # Build all the formats formats: all python: install: - requirements: requirements/doc.txt - method: pip path: . scriv-1.7.0/CHANGELOG.rst000066400000000000000000000453641500123625400147000ustar00rootroot00000000000000.. this will be appended to README.rst Changelog ========= .. All enhancements and patches to scriv will be documented in this file. It adheres to the structure of http://keepachangelog.com/ , but in reStructuredText instead of Markdown (for ease of incorporation into Sphinx documentation and the PyPI description). This project adheres to Semantic Versioning (http://semver.org/). Unreleased ---------- See the fragment files in the `changelog.d directory`_. .. _changelog.d directory: https://github.com/nedbat/scriv/tree/master/changelog.d .. scriv-insert-here .. _changelog-1.7.0: 1.7.0 — 2025-04-20 ------------------ Added ..... - The GitHub release template now can use ``{{title}}`` to get the title of the changelog entry. - The ``format`` setting is now defaulted based on the ``changelog`` setting. Previously, ``changelog=README.md`` would still use .rst formatting. Now it will use Markdown. Changed ....... - Two settings have new names to better reflect what scriv does. The ``output_file`` setting is now called ``changelog`` and the ``insert_marker`` setting is now called ``start_marker``. The old names will continue to work. Closes `issue 77`_. .. _issue 77: https://github.com/nedbat/scriv/issues/77 .. _changelog-1.6.2: 1.6.2 — 2025-03-30 ------------------ Fixed ..... - Replaced the invalid GitHub config key ``scriv.user_nick`` with ``scriv.user-nick``. Fixes `issue 130`_. Thanks, `Mark Dickinson `_. .. _issue 130: https://github.com/nedbat/scriv/issues/130 .. _pull 131: https://github.com/nedbat/scriv/pull/131/files .. _changelog-1.6.1: 1.6.1 — 2025-03-24 ------------------ Fixed ..... - Corrected two minor packaging errors: the Mastodon badge and a typo in the short description. .. _changelog-1.6.0: 1.6.0 — 2025-03-24 ------------------ Added ..... - Add a ``print`` command that can write changelog entries to standard out or to a file, closing `issue 115`_. Thanks, `Kurt McKee `_ Changed ....... - Dropped support for Python 3.7 and 3.8, and added 3.13. Fixed ..... - A final newline is no longer stripped when rendering the new fragment template, fixing `issue 108`_. - Configuration setting ``md_header_level`` is allowed to be an integer in TOML files, closing `issue 90`_. Thanks, `Michael Makukha `_. .. _issue 90: https://github.com/nedbat/scriv/issues/90 .. _issue 108: https://github.com/nedbat/scriv/issues/108 .. _issue 115: https://github.com/nedbat/scriv/issues/115 .. _pull 137: https://github.com/nedbat/scriv/pull/137 .. _pull 140: https://github.com/nedbat/scriv/pull/140 .. _changelog-1.5.1: 1.5.1 — 2023-12-14 ------------------ Fixed ..... - Fixed the documentation build on ReadTheDocs. Fixes `issue 118`_. .. _issue 118: https://github.com/nedbat/scriv/issues/118 .. _changelog-1.5.0: 1.5.0 — 2023-10-18 ------------------ Added ..... - RST to Markdown conversion can now be stricter. Using the ``--fail-if-warn`` option on the ``scriv github-releases`` command will fail the command if your RST conversion generates warnings, for example due to malformed link references. - The ``scriv github-release`` command now has a ``--check-links`` option to check URLs. Each is fetched, and if an error occurs, warnings will show the URLs that didn't succeed. Fixed ..... - Commands no longer display full tracebacks for exceptions raised by scriv code. .. _changelog-1.4.0: 1.4.0 — 2023-10-12 ------------------ Added ..... - Literals can be extracted from .cabal files. Thanks `Javier Sagredo `_. - Use the git config ``scriv.user_nick`` for the user nick part of the fragment file. Thanks to `Ronny Pfannschmidt `_, fixing `issue 103`_. - Settings can now be prefixed with ``command:`` to execute the rest of the setting as a shell command. The output of the command will be used as the value of the setting. Fixed ..... - If there are no changelog fragments, ``scriv collect`` now exits with status code of 2, fixing `issue 110`_. - Changelogs with non-version headings now produce an understandable error message from ``scriv collect``, thanks to `James Gerity `_, fixing `issue 100`_. .. _pull 91: https://github.com/nedbat/scriv/pull/91 .. _issue 100: https://github.com/nedbat/scriv/issues/100 .. _pull 101: https://github.com/nedbat/scriv/pull/101 .. _issue 103: https://github.com/nedbat/scriv/pull/103 .. _pull 106: https://github.com/nedbat/scriv/pull/106 .. _issue 110: https://github.com/nedbat/scriv/issues/110 .. _changelog-1.3.1: 1.3.1 — 2023-04-16 ------------------ Fixed ..... - The Version class introduced in 1.3.0 broke the ``scriv github-release`` command. This is now fixed. .. _changelog-1.3.0: 1.3.0 — 2023-04-16 ------------------ Added ..... - ``.cfg`` files can now be read with ``literal:`` settings, thanks to `Matias Guijarro `_. .. _pull 88: https://github.com/nedbat/scriv/pull/88 Fixed ..... - In compliance with `PEP 440`_, comparing version numbers now ignores a leading "v" character. This makes scriv more flexible about how you present version numbers in various places (code literals, changelog entries, git tags, and so on). Fixes `issue 89`_. .. _PEP 440: https://peps.python.org/pep-0440/ .. _issue 89: https://github.com/nedbat/scriv/issues/89 .. _changelog-1.2.1: 1.2.1 — 2023-02-18 ------------------ Fixed ..... - Scriv would fail trying to import tomllib on Python <3.11 if installed without the ``[toml]`` extra. This is now fixed, closing `issue 80`_. - Settings specified as ``file:`` will now search in the changelog directory and then the current directory for the file. The only exception is if the first component is ``.`` or ``..``, then only the current directory is considered. Fixes `issue 82`_. - Python variables with type annotations can now be read with ``literal:`` settings, fixing `issue 85`_. - Error messages for mis-formed ``literal:`` configuration values are more precise, as requested in `issue 84`_. - Error messages from settings validation are ScrivExceptions now, and report configuration problems more clearly and earlier in some cases. .. _issue 80: https://github.com/nedbat/scriv/issues/80 .. _issue 82: https://github.com/nedbat/scriv/issues/82 .. _issue 84: https://github.com/nedbat/scriv/issues/84 .. _issue 85: https://github.com/nedbat/scriv/issues/85 .. _changelog-1.2.0: 1.2.0 — 2023-01-18 ------------------ Added ..... - ``scriv github-release`` now has a ``--repo=`` option to specify which GitHub repo to use when you have multiple remotes. Changed ....... - Improved the error messages from ``scriv github-release`` when a GitHub repo can't be identified among the git remotes. .. _changelog-1.1.0: 1.1.0 — 2023-01-16 ------------------ Added ..... - The ``scriv github-release`` command has a new setting, ``ghrel_template``. This is a template to use when building the release text, to add text before or after the Markdown extracted from the changelog. - The ``scriv github-release`` command now has a ``--dry-run`` option to show what would happen, without posting to GitHub. Changed ....... - File names specified for ``file:`` settings will be interpreted relative to the current directory if they have path components. If the file name has no slashes or backslashes, then the old behavior remains: the file will be found in the fragment directory, or as a built-in template. - All exceptions raised by Scriv are now ScrivException. Fixed ..... - Parsing changelogs now take the `insert-marker` setting into account. Only content after the insert-marker line is parsed. - More internal activities are logged, to help debug operations. .. _changelog-1.0.0: 1.0.0 — 2022-12-03 ------------------ Added ..... - Now literal configuration settings can be read from YAML files. Closes `issue 69`_. Thanks, `Florian Küpper `_. .. _pull 70: https://github.com/nedbat/scriv/pull/70 .. _issue 69: https://github.com/nedbat/scriv/issues/69 Fixed ..... - Fixed truncated help summaries by shortening them, closing `issue 63`_. .. _issue 63: https://github.com/nedbat/scriv/issues/63 .. _changelog-0.17.0: 0.17.0 — 2022-09-18 ------------------- Added ..... - The ``collect`` command now has a ``--title=TEXT`` option to provide the exact text to use as the title of the new changelog entry. Finishes `issue 48`_. .. _issue 48: https://github.com/nedbat/scriv/issues/48 Changed ....... - The ``github_release`` command now only considers the top-most entry in the changelog. You can use the ``--all`` option to continue the old behavior of making or updating GitHub releases for all of the entries. This change makes it easier for projects to start using scriv with an existing populated changelog file. Closes `issue 57`_. .. _issue 57: https://github.com/nedbat/scriv/issues/57 Fixed ..... - If there were no fragments to collect, `scriv collect` would make a new empty section in the changelog. This was wrong, and is now fixed. Now the changelog remains unchanged in this case. Closes `issue 55`_. .. _issue 55: https://github.com/nedbat/scriv/issues/55 - The ``github-release`` command will now issue a warning for changelog entries that have no version number. These can't be made into releases, so they are skipped. (`issue 56`_). .. _issue 56: https://github.com/nedbat/scriv/issues/56 - ``scriv collect`` will end with an error now if the version number would duplicate a version number on an existing changelog entry. Fixes `issue 26`_. .. _issue 26: https://github.com/nedbat/scriv/issues/26 .. _changelog-0.16.0: 0.16.0 — 2022-07-24 ------------------- Added ..... - The ``github_release`` command will use a GitHub personal access token stored in the GITHUB_TOKEN environment variable, or from a .netrc file. Fixed ..... - The github_release command was using `git tags` as a command when it should have used `git tag`. - Anchors in the changelog were being included in the previous sections when creating GitHub releases. This has been fixed, closing `issue 53`_. .. _issue 53: https://github.com/nedbat/scriv/issues/53 .. _changelog-0.15.2: 0.15.2 — 2022-06-18 ------------------- Fixed ..... - Quoted commands failed, so we couldn't determine the GitHub remote. .. _changelog-0.15.1: 0.15.1 — 2022-06-18 ------------------- Added ..... - Added docs for ``scriv github-release``. Fixed ..... - Call pandoc properly on Windows for the github_release command. .. _changelog-0.15.0: 0.15.0 — 2022-04-24 ------------------- Removed ....... - Dropped support for Python 3.6. Added ..... - The `github-release` command parses the changelog and creates GitHub releases from the entries. Changed entries will update the corresponding release. - Added a ``--version`` option. Changed ....... - Parsing of fragments now only attends to the top-level section headers, and includes nested headers instead of splitting on all headers. .. _changelog-0.14.0: 0.14.0 — 2022-03-23 ------------------- Added ..... - Add an anchor before each version section in the output of ``scriv collect`` so URLs for the sections are predictable and stable for each new version (Fixes `issue 46`_). Thanks Abhilash Raj and Rodrigo Girão Serrão. Fixed ..... - Markdown fragments weren't combined properly. Now they are. Thanks Rodrigo Girão Serrão. .. _issue 46: https://github.com/nedbat/scriv/issues/46 0.13.0 — 2022-01-23 ------------------- Added ..... - Support finding version information in TOML files (like ``pyproject.toml``) using the ``literal`` configuration directive. Thanks, Kurt McKee 0.12.0 — 2021-07-28 ------------------- Added ..... - Fragment files in the fragment directory will be skipped if they match the new configuration value ``skip_fragments``, a glob pattern. The default value is "README.*". This lets you put a README.md file in that directory to explain its purpose, as requested in `issue 40`_. .. _issue 40: https://github.com/nedbat/scriv/issues/40 Changed ....... - Switched from "toml" to "tomli" for reading TOML files. Fixed ..... - Setting ``format=md`` didn't properly cascade into other default settings, leaving you with RST settings that needed to be explicitly overridden (`issue 39`_). This is now fixed. .. _issue 39: https://github.com/nedbat/scriv/issues/39 0.11.0 — 2021-06-22 ------------------- Added ..... - A new poorly documented API is available. See the Scriv, Changelog, and Fragment classes in the scriv.scriv module. Changed ....... - Python 3.6 is now the minimum supported Python version. Fixed ..... - The changelog is now always written as UTF-8, regardless of the default encoding of the system. Thanks, Hei (yhlam). 0.10.0 — 2020-12-27 ------------------- Added ..... - Settings can now be read from a pyproject.toml file. Install with the "[toml]" extra to be sure TOML support is available. Closes `issue 9`_. .. _issue 9: https://github.com/nedbat/scriv/issues/9 - Added the Philosophy section of the docs. Changed ....... - The default entry header no longer puts the version number in square brackets: this was a misunderstanding of the keepachangelog formatting. - Respect the existing newline style of changelog files. (`#14`_) This means that a changelog file with Linux newlines on a Windows platform will be updated with Linux newlines, not rewritten with Windows newlines. Thanks, Kurt McKee. .. _#14: https://github.com/nedbat/scriv/issues/14 Fixed ..... - Support Windows' directory separator (``\``) in unit test output. (`#15`_) This allows the unit tests to run in Windows environments. Thanks, Kurt McKee. - Explicitly specify the directories and files that Black should scan. (`#16`_) This prevents Black from scanning every file in a virtual environment. Thanks, Kurt McKee. - Using "literal:" values in the configuration file didn't work on Python 3.6 or 3.7, as reported in `issue 18`_. This is now fixed. .. _#15: https://github.com/nedbat/scriv/issues/15 .. _#16: https://github.com/nedbat/scriv/issues/16 .. _issue 18: https://github.com/nedbat/scriv/issues/18 0.9.2 — 2020-08-29 ------------------ - Packaging fix. 0.9.0 — 2020-08-29 ------------------ Added ..... - Markdown format is supported, both for fragments and changelog entries. - Fragments can be mixed (some .rst and some .md). They will be collected and output in the format configured in the settings. - Documentation. - "python -m scriv" now works. Changed ....... - The version number is displayed in the help message. 0.8.1 — 2020-08-09 ------------------ Added ..... - When editing a new fragment during "scriv create", if the edited fragment has no content (only comments or blank lines), then the create operation will be aborted, and the file will be removed. (Closes `issue 2`_.) .. _issue 2: https://github.com/nedbat/scriv/issues/2 Changed ....... - If the fragment directory doesn't exist, a simple direct message is shown, rather than a misleading FileNotFound error (closes `issue 1`_). .. _issue 1: https://github.com/nedbat/scriv/issues/1 Fixed ..... - When not using categories, comments in fragment files would be copied to the changelog file (`issue 3`_). This is now fixed. .. _issue 3: https://github.com/nedbat/scriv/issues/3 - RST syntax is better understood, so that hyperlink references and directives will be preserved. Previously, they were mistakenly interpreted as comments and discarded. 0.8.0 — 2020-08-04 ------------------ Added ..... - Added the `collect` command. - Configuration is now read from setup.cfg or tox.ini. - A new configuration setting, rst_section_char, determines the character used in the underlines for the section headings in .rst files. - The `new_entry_template` configuration setting is the name of the template file to use when creating new entries. The file will be found in the `fragment_directory` directory. The file name defaults to ``new_entry.FMT.j2``. If the file doesn't exist, an internal default will be used. - Now the collect command also includes a header for the entire entry. The underline is determined by the "rst_header_char" settings. The heading text is determined by the "header" setting, which defaults to the current date. - The categories list in the config can be empty, meaning entries are not categorized. - The create command now accepts --edit (to open the new entry in your text editor), and --add (to "git add" the new entry). - The collect command now accepts --edit (to open the changelog file in an editor after the new entries have been collected) and --add (to git-add the changelog file and git rm the entries). - The names of the main git branches are configurable as "main_branches" in the configuration file. The default is "master", "main", and "develop". - Configuration values can now be read from files by prefixing them with "file:". File names will be interpreted relative to the changelog.d directory, or will be found in a few files installed with scriv. - Configuration values can interpolate the currently configured format (rst or md) with "${config:format}". - The default value for new templates is now "file: new_entry.${config:format}.j2". - Configuration values can be read from string literals in Python code with a "literal:" prefix. - "version" is now a configuration setting. This will be most useful when used with the "literal:" prefix. - By default, the title of collected changelog entries includes the version if it's defined. - The collect command now accepts a ``--version`` option to set the version name used in the changelog entry title. Changed ....... - RST now uses minuses instead of equals. - The `create` command now includes the time as well as the date in the entry file name. - The --delete option to collect is now called --keep, and defaults to False. By default, the collected entry files are removed. - Created file names now include the seconds from the current time. - "scriv create" will refuse to overwrite an existing entry file. - Made terminology more uniform: files in changelog.d are "fragments." When collected together, they make one changelog "entry." - The title text for the collected changelog entry is now created from the "entry_title_template" configuration setting. It's a Jinja2 template. - Combined the rst_header_char and rst_section_char settings into one: rst_header_chars, which much be exactly two characters. - Parsing RST fragments is more flexible: the sections can use any valid RST header characters for the underline. Previously, it had to match the configured RST header character. Fixed ..... - Fragments with no category header were being dropped if categories were in use. This is now fixed. Uncategorized fragments get sorted before any categorized fragments. 0.1.0 — 2019-12-30 ------------------ * Doesn't really do anything yet. scriv-1.7.0/LICENSE.txt000066400000000000000000000237011500123625400144710ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS scriv-1.7.0/MANIFEST.in000066400000000000000000000006061500123625400144030ustar00rootroot00000000000000include .editorconfig include .ignore include .mailmap include .readthedocs.yaml include CHANGELOG.rst include LICENSE.txt include Makefile include pylintrc include README.rst include tox.ini recursive-include changelog.d * recursive-include docs Makefile *.py *.rst recursive-include docs/_static * recursive-include requirements *.in *.txt recursive-include tests *.py prune doc/_build scriv-1.7.0/Makefile000066400000000000000000000122651500123625400143110ustar00rootroot00000000000000# Makefile for scriv # # To release: # - increment the version in src/scriv/__init__.py # - scriv collect # - commit changes # - make check_release # - make release .DEFAULT_GOAL := help # For opening files in a browser. Use like: $(BROWSER)relative/path/to/file.html BROWSER := python -m webbrowser file://$(CURDIR)/ # A command to get the current version. A little slow, but only run when needed. VERSION := $$(python -c "import build.util as bu; print(bu.project_wheel_metadata('.')['Version'])") .PHONY: help clean sterile help: ## display this help message @echo "Please use \`make ' where is one of" @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort clean: ## remove generated byte code, coverage reports, and build artifacts find . -name '__pycache__' -exec rm -rf {} + find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + -coverage erase rm -fr coverage.json rm -fr build/ rm -fr dist/ rm -fr *.egg-info rm -fr htmlcov/ rm -fr .*_cache/ cd docs; make clean sterile: clean ## remove absolutely all built artifacts rm -fr .tox .PHONY: coverage docs upgrade diff_upgrade coverage: clean ## generate and view HTML coverage report tox -e py39,py313,coverage $(BROWSER)htmlcov/index.html docs: botedits ## generate Sphinx HTML documentation, including API docs tox -e docs $(BROWSER)docs/_build/html/index.html PIP_COMPILE = pip-compile --upgrade --resolver=backtracking -c requirements/constraints.txt --no-strip-extras upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in pip install -qr requirements/pip-tools.txt # Make sure to compile files after any other files they include! $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in $(PIP_COMPILE) -o requirements/base.txt requirements/base.in $(PIP_COMPILE) -o requirements/test.txt requirements/test.in $(PIP_COMPILE) -o requirements/doc.txt requirements/doc.in $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in $(PIP_COMPILE) -o requirements/tox.txt requirements/tox.in $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in diff_upgrade: ## summarize the last `make upgrade` @# The sort flags sort by the package name first, then by the -/+, and @# sort by version numbers, so we get a summary with lines like this: @# -bashlex==0.16 @# +bashlex==0.17 @# -build==0.9.0 @# +build==0.10.0 @git diff -U0 | grep -v '^@' | grep == | sort -k1.2,1.99 -k1.1,1.1r -u -V .PHONY: botedits quality requirements test test-all validate botedits: ## make source edits by tools python -m black --line-length=80 src/scriv tests docs python -m cogapp -crP docs/*.rst quality: ## check coding style with pycodestyle and pylint tox -e quality requirements: ## install development environment requirements pip install -qr requirements/pip-tools.txt pip-sync requirements/dev.txt pip install -e . test: ## run tests in the current virtualenv tox -e py39 test-all: ## run tests on every supported Python combination tox validate: clean botedits quality test ## run tests and quality checks .PHONY: dist pypi testpypi tag gh_release comment_text dist: ## build the distributions python -m build --sdist --wheel pypi: ## upload the built distributions to PyPI. python -m twine upload --verbose dist/* testpypi: ## upload the distrubutions to PyPI's testing server. python -m twine upload --verbose --repository testpypi dist/* tag: ## make a git tag with the version number git tag -s -m "Version $(VERSION)" $(VERSION) git push --all gh_release: ## make a GitHub release python -m scriv github-release --all --fail-if-warn --check-links comment_text: @echo "Use this to comment on issues and pull requests:" @echo "This is now released as part of [scriv $(VERSION)](https://pypi.org/project/scriv/$(VERSION))." .PHONY: release check_release _check_credentials _check_manifest _check_tree _check_version _check_scriv _check_links release: _check_credentials clean check_release dist pypi tag gh_release comment_text ## do all the steps for a release check_release: _check_manifest _check_tree _check_version _check_scriv _check_links ## check that we are ready for a release @echo "Release checks passed" _check_credentials: @if [[ -z "$$TWINE_PASSWORD" ]]; then \ echo 'Missing TWINE_PASSWORD: opvars'; \ exit 1; \ fi @if [[ -z "$$GITHUB_TOKEN" ]]; then \ echo 'Missing GITHUB_TOKEN: opvars github'; \ exit 1; \ fi _check_manifest: python -m check_manifest _check_tree: @if [[ -n $$(git status --porcelain) ]]; then \ echo 'There are modified files! Did you forget to check them in?'; \ exit 1; \ fi _check_version: @if [[ $$(git tags | grep -q -w $(VERSION) && echo "x") == "x" ]]; then \ echo 'A git tag for this version exists! Did you forget to bump the version in src/scriv/__init__.py?'; \ exit 1; \ fi _check_scriv: @if [[ $$(find -E changelog.d -regex '.*\.(md|rst)$$') ]]; then \ echo 'There are scriv fragments! Did you forget `scriv collect`?'; \ exit 1; \ fi _check_links: python -m scriv github-release --dry-run --fail-if-warn --check-links scriv-1.7.0/README.rst000066400000000000000000000075631500123625400143450ustar00rootroot00000000000000##### Scriv ##### Scriv changelog management tool .. begin-badges | |pypi-badge| |ci-badge| |coverage-badge| |doc-badge| | |pyversions-badge| |license-badge| | |sponsor-badge| |bluesky-nedbat| |mastodon-nedbat| .. end Overview ======== Scriv is a command-line tool for helping developers maintain useful changelogs. It manages a directory of changelog fragments. It aggregates them into entries in a CHANGELOG file. Getting Started =============== Scriv writes changelog fragments into a directory called "changelog.d". Start by creating this directory. (By the way, like many aspects of scriv's operation, you can choose a different name for this directory.) To make a new changelog fragment, use the ``scriv create`` command. It will make a new file with a filename using the current date and time, your GitHub or Git user name, and your branch name. Changelog fragments should be committed along with all the other changes on your branch. When it is time to release your project, the ``scriv collect`` command aggregates all the fragments into a new entry in your changelog file. You can also choose to publish your changelog entries as GitHub releases with the ``scriv github-release`` command. It parses the changelog file and creates or updates GitHub releases to match. It can be used even with changelog files that were not created by scriv. Documentation ============= Full documentation is at https://scriv.readthedocs.org. License ======= The code in this repository is licensed under the Apache Software License 2.0 unless otherwise noted. Please see ``LICENSE.txt`` for details. How To Contribute ================= Contributions are very welcome. Thanks to all the contributors so far: .. begin-contributors | Ned Batchelder | Abhilash Raj | Agustín Piqueres | Alyssa Coughlan | Flo Kuepper | James Gerity | Javier Sagredo | Kurt McKee | Mark Dickinson | Matias Guijarro | Michael Makukha | Rodrigo Girão Serrão | Ronny Pfannschmidt .. end .. begin-badge-links .. |pypi-badge| image:: https://img.shields.io/pypi/v/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: PyPI .. |ci-badge| image:: https://github.com/nedbat/scriv/workflows/Test%20Suite/badge.svg :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Build status .. |coverage-badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/5a304c1c779d4bcc57be95f847e9327f/raw/covbadge.json :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Coverage .. |doc-badge| image:: https://readthedocs.org/projects/scriv/badge/?version=latest :target: http://scriv.readthedocs.io/en/latest/ :alt: Documentation .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: Supported Python versions .. |license-badge| image:: https://img.shields.io/github/license/nedbat/scriv.svg :target: https://github.com/nedbat/scriv/blob/master/LICENSE.txt :alt: License .. |bluesky-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&color=96a3b0&labelColor=3686f7&logo=icloud&logoColor=white&label=@nedbat&url=https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2Fapp.bsky.actor.getProfile%3Factor=nedbat.com&query=followersCount :target: https://bsky.app/profile/nedbat.com :alt: nedbat on Bluesky .. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@nedbat&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=nedbat :target: https://hachyderm.io/@nedbat :alt: nedbat on Mastodon .. |sponsor-badge| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub .. end scriv-1.7.0/changelog.d/000077500000000000000000000000001500123625400150145ustar00rootroot00000000000000scriv-1.7.0/changelog.d/README.txt000066400000000000000000000001011500123625400165020ustar00rootroot00000000000000This directory will hold the changelog entries managed by scriv. scriv-1.7.0/changelog.d/ghrel_template.md.j2000066400000000000000000000002661500123625400206500ustar00rootroot00000000000000:arrow_right:  PyPI page: [scriv {{version}}](https://pypi.org/project/scriv/{{version}}). :arrow_right:  To install: `python3 -m pip install scriv=={{version}}` {{body}} scriv-1.7.0/docs/000077500000000000000000000000001500123625400135735ustar00rootroot00000000000000scriv-1.7.0/docs/Makefile000066400000000000000000000037501500123625400152400ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/ .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." scriv-1.7.0/docs/_static/000077500000000000000000000000001500123625400152215ustar00rootroot00000000000000scriv-1.7.0/docs/_static/theme_overrides.css000066400000000000000000000005161500123625400211210ustar00rootroot00000000000000/* override table width restrictions */ .wy-table-responsive table td, .wy-table-responsive table th { /* !important prevents the common CSS stylesheets from overriding this as on RTD they are loaded after this stylesheet */ white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } scriv-1.7.0/docs/changelog.rst000066400000000000000000000000361500123625400162530ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst scriv-1.7.0/docs/commands.rst000066400000000000000000000224121500123625400161270ustar00rootroot00000000000000######## Commands ######## .. [[[cog # Force help text to be wrapped narrow enough to not trigger doc8 warnings. import os os.environ["COLUMNS"] = "78" import contextlib import io import textwrap from scriv.cli import cli def show_help(cmd): with contextlib.redirect_stdout(io.StringIO()) as help_out: with contextlib.suppress(SystemExit): cli([cmd, "--help"]) help_text = help_out.getvalue() help_text = help_text.replace("python -m cogapp", "scriv") print("\n.. code::\n") print(f" $ scriv {cmd} --help") print(textwrap.indent(help_text, " ").rstrip()) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) .. _cmd_create: scriv create ============ .. [[[cog show_help("create") ]]] .. code:: $ scriv create --help Usage: scriv create [OPTIONS] Create a new changelog fragment. Options: --add / --no-add 'git add' the created file. --edit / --no-edit Open the created file in your text editor. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: 45edec1fd1ebc343358cbf774ba5a49c) The create command creates new :ref:`fragments `. File creation ------------- Fragments are created in the changelog.d directory. The name of the directory can be configured with the :ref:`config_fragment_directory` setting. The file name starts with the current date and time, so that entries can later be collected in chronological order. To help make the files understandable, the file name also includes the creator's git name, and the branch name you are working on. "Main" branch names aren't included, to cut down on uninteresting noise. The branch names considered uninteresting are settable with the :ref:`config_main_branches` setting. The initial contents of the fragment file are populated from the :ref:`config_new_fragment_template` template. The format is either reStructuredText or Markdown, selectable with the :ref:`config_format` setting. The default new fragment templates create empty sections for each :ref:`category `. Uncomment the one you want to use, and create a bullet for the changes you are describing. If you need a different template for new fragments, you can create a `Jinja`_ template and name it in the :ref:`config_new_fragment_template` setting. Editing ------- If ``--edit`` is provided, or if ``scriv.create.edit`` is set to true in your :ref:`git settings `, scriv will launch an editor for you to edit the new fragment. Scriv uses the same editor that git launches for commit messages. The format of the fragment should be sections for the categories, with bullets for each change. The file is re-parsed when it is collected, so the specifics of things like header underlines don't have to match the changelog file, that will be adjusted later. Once you save and exit the editor, scriv will continue working on the file. If the file is empty because you removed all of the non-comment content, scriv will stop. Adding ------ If ``--add`` is provided, or if ``scriv.create.add`` is set to true in your :ref:`git settings `, scriv will "git add" the new file so that it is ready to commit. .. _cmd_collect: scriv collect ============= .. [[[cog show_help("collect") ]]] .. code:: $ scriv collect --help Usage: scriv collect [OPTIONS] Collect and combine fragments into the changelog. Options: --add / --no-add 'git add' the updated changelog file and removed fragments. --edit / --no-edit Open the changelog file in your text editor. --title TEXT The title text to use for this entry. --keep Keep the fragment files that are collected. --version TEXT The version name to use for this entry. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: e93ca778396310ce406f1cc439cefdd4) The collect command aggregates all the current fragments into the changelog file. Entry Creation -------------- All of the .rst or .md files in the fragment directory are read, parsed, and re-assembled into a changelog entry. The entry's title is determined by the :ref:`config_entry_title_template` setting. The default uses the version string (if one is specified in the :ref:`config_version` setting) and the current date. Instead of using the title template, you can provide an exact title to use for the new entry with the ``--title`` option. The output file is specified by the :ref:`config_changelog` setting. Scriv looks in the file for a special marker (usually in a comment) to determine where to insert the new entry. The marker is "scriv-insert-here", but can be changed with the :ref:`config_start_marker` setting. Using a marker like this, you can have your changelog be just part of a larger README file. If there is no marker in the file, the new entry is inserted at the top of the file. Fragment Deletion ----------------- The fragment files that are read will be deleted, because they are no longer needed. If you would prefer to keep the fragment files, use the ``--keep`` option. Editing ------- If ``--edit`` is provided, or if ``scriv.collect.edit`` is set to true in your :ref:`git settings `, scriv will launch an editor for you to edit the changelog file. Mostly you shouldn't need to do this, but you might want to make some tweaks. Scriv uses the same editor that git launches for commit messages. Adding ------ If ``--add`` is provided, or if ``scriv.collect.add`` is set to true in your :ref:`git settings `, scriv will "git add" the updates to the changelog file, and the fragment file deletions, so that they are ready to commit. .. _cmd_github_release: scriv github-release ==================== .. [[[cog show_help("github-release") ]]] .. code:: $ scriv github-release --help Usage: scriv github-release [OPTIONS] Create GitHub releases from the changelog. Only the most recent changelog entry is used, unless --all is provided. Options: --all Use all of the changelog entries. --check-links Check that links are valid (EXPERIMENTAL). --dry-run Don't post to GitHub, just show what would be done. --fail-if-warn Fail if a conversion generates warnings. --repo TEXT The GitHub repo (owner/reponame) to create the release in. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: ec63a3f79902b40a74e633cdeb1bf3dc) The ``github-release`` command reads the changelog file, parses it into entries, and then creates or updates GitHub releases to match. Only the most recent changelog entry is used, unless ``--all`` is provided. An entry must have a version number in the title, and that version number must correspond to a git tag. For example, this changelog entry with the title ``v1.2.3 -- 2022-04-06`` will be processed and the version number will be "v1.2.3". If there's a "v1.2.3" git tag, then the entry is a valid release. If there's no detectable version number in the header, or there isn't a git tag with the same number, then the entry can't be created as a GitHub release. The ``--fail-if-warn`` option will end the command if a format conversion generates a warning, usually because of a missing reference. The ``--check-links`` option will find the URLs in the release description, and check if they are valid. Warnings are displayed for invalid URLs, but the command still creates the release. This command is independent of the other commands. It can be used with a hand-edited changelog file that wasn't created with scriv. For writing to GitHub, you need a GitHub personal access token, either stored in your .netrc file, or in the GITHUB_TOKEN environment variable. The GitHub repo will be determined by examining the git remotes. If there is just one GitHub repo in the remotes, it will be used to create the release. You can explicitly specify a repo in ``owner/reponame`` form with the ``--repo=`` option if needed. If your changelog file is in reStructuredText format, you will need `pandoc`_ 2.11.2 or later installed for the command to work. .. _pandoc: https://pandoc.org/ scriv print =========== .. [[[cog show_help("print") ]]] .. code:: $ scriv print --help Usage: scriv print [OPTIONS] Print collected fragments, or print an entry from the changelog. Options: --version TEXT The version of the changelog entry to extract. --output PATH The path to a file to write the output to. -v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG --help Show this message and exit. .. [[[end]]] (checksum: f652a3470da5f726b13ba076471b2444) The ``print`` command writes a changelog entry to standard out. If ``--output`` is provided, the changelog entry is written to the given file. If ``--version`` is given, the requested changelog entry is extracted from the CHANGELOG. If not, then the changelog entry is generated from uncollected fragment files. .. include:: include/links.rst scriv-1.7.0/docs/concepts.rst000066400000000000000000000026321500123625400161460ustar00rootroot00000000000000######## Concepts ######## .. _fragments: Fragments ========= Fragments are files describing your latest work, created by the ":ref:`cmd_create`" command. The files are created in the changelog.d directory (settable with :ref:`config_fragment_directory`). Typically, they are committed with the code change itself, then later aggregated into the changelog file with ":ref:`cmd_collect`". .. _categories: Categories ========== Changelog entries can be categorized, for example as additions, fixes, removals, and breaking changes. The list of categories is settable with the :ref:`config_categories` setting. If you are using categories in your project, new fragments will be pre-populated with all the categories, commented out. While editing the fragment, you provide your change information in the appropriate category. When the fragments are collected, they are grouped by category into a single changelog entry. Any fragments that do not specify a category are included as top-level release notes directly under the release heading. You can choose not to use categories by setting the :ref:`config_categories` setting to empty (all notes will appear as top-level release notes). .. _entries: Entries ======= Fragments are collected into changelog entries with the ":ref:`cmd_collect`" command. The fragments are combined in each category, in chronological order. The entry is given a header with version and date. scriv-1.7.0/docs/conf.py000066400000000000000000000314241500123625400150760ustar00rootroot00000000000000# pylint: disable=invalid-name, redefined-builtin """ Scriv documentation build configuration file. This file is execfile()d with the current directory set to its containing dir. Note that not all possible configuration values are present in this autogenerated file. All configuration values have a default; values that are commented out serve to show the default. """ import os import re import sys # import sphinx_rtd_theme def get_version(*file_paths): """ Extract the version string from a file. """ filename = os.path.join(os.path.dirname(__file__), *file_paths) with open(filename, encoding="utf-8") as version_file: version_text = version_file.read() version_match = re.search( r"^__version__ = ['\"]([^'\"]*)['\"]", version_text, re.M ) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(REPO_ROOT) VERSION = get_version("../src/scriv", "__init__.py") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.ifconfig", "sphinx.ext.napoleon", "sphinx_rtd_theme", ] # A list of warning types to suppress arbitrary warning messages. suppress_warnings = [ "image.nonlocal_uri", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Scriv" copyright = "2019\N{EN DASH}2025, Ned Batchelder" author = "Ned Batchelder" project_title = "scriv" documentation_title = f"{project_title}" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = VERSION # The full version, including alpha/beta/rc tags. release = VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [ "_build", "include/*", "Thumbs.db", ".DS_Store", ] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = 'scriv v0.1.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = f"{project}doc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_target = f"{project}.tex" latex_documents = [ (master_doc, latex_target, documentation_title, author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # It false, will not define \strong, \code, itleref, \crossref ... but only # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added # packages. # # latex_keep_old_macro_names = True # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, project_title, documentation_title, [author], 1)] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, project_title, documentation_title, author, project_title, "Scriv changelog management tool", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # The basename for the epub file. It defaults to the project name. # epub_basename = project # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to save # visual space. # # epub_theme = 'epub' # The language of the text. It defaults to the language option # or 'en' if the language is not set. # # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # # epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. # # epub_tocdepth = 3 # Allow duplicate toc entries. # # epub_tocdup = True # Choose between 'default' and 'includehidden'. # # epub_tocscope = 'default' # Fix unsupported image types using the Pillow. # # epub_fix_images = False # Scale large images. # # epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. # # epub_show_urls = 'inline' # If false, no index is generated. # # epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), } scriv-1.7.0/docs/configuration.rst000066400000000000000000000260211500123625400171750ustar00rootroot00000000000000############# Configuration ############# .. highlight:: ini Scriv tries hard to be adaptable to your project's needs. Many aspects of its behavior can be customized with a settings file. Files Read ========== Scriv will read settings from any of these files: - setup.cfg - tox.ini - pyproject.toml - scriv.ini in the fragment directory ("changelog.d/" by default) In .ini or .cfg files, scriv will read settings from a section named either ``[scriv]`` or ``[tool.scriv]``. A .toml file will only be read if the tomli or tomllib modules is available. You can install scriv with the ``[toml]`` extra to install tomli, or tomllib is available with Python 3.11 or greater. In a .toml file, settings will only be read from the ``[tool.scriv]`` section. All of the possible files will be read, and settings will cascade. So for example, setup.cfg can set the fragment directory to "scriv.d", then "scriv.d/scriv.ini" will be read. The settings examples here show .ini syntax. If you are using a pyproject.toml file for settings, you will need to adjust for TOML syntax. This .ini example:: [scriv] version = literal: pyproject.toml: project.version would become: .. code-block:: toml [tool.scriv] version = "literal: pyproject.toml: project.version" Settings Syntax =============== Settings use the usual syntax, but with some extra features: - A prefix of ``file:`` reads the setting from a file. - A prefix of ``literal:`` reads a literal data from a source file. - A prefix of ``command:`` runs the command and uses the output as the setting. - Value substitutions can make a setting depend on another setting. These are each explained below: File Prefix ----------- A ``file:`` prefix means the setting is a file name or path, and the actual setting value will be read from that file. The file name will be searched for in three places: the fragment directory (changelog.d by default), the current directory, or one of a few built-in templates. If the first path component is ``.`` or ``..``, then only the current directory is considered. Scriv provides two built-in templates: .. [[[cog import textwrap def include_file(fname): """Include a source file into the docs as a code block.""" print(".. code-block:: jinja\n") with open(fname) as f: print(textwrap.indent(f.read(), prefix=" ")) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) - ``new_fragment.md.j2``: The default Jinja template for new Markdown fragments: .. [[[cog include_file("src/scriv/templates/new_fragment.md.j2") ]]] .. code-block:: jinja {% for cat in config.categories -%} {% endfor -%} .. [[[end]]] (checksum: 5ea187a050bfc23014591238b22520ff) - ``new_fragment.rst.j2``: The default Jinja template for new reStructuredText fragments: .. [[[cog include_file("src/scriv/templates/new_fragment.rst.j2") ]]] .. code-block:: jinja .. A new scriv changelog fragment. {% if config.categories -%} .. .. Uncomment the section that is right (remove the leading dots). .. For top level release notes, leave all the headers commented out. .. {% for cat in config.categories -%} .. {{ cat }} .. {{ config.rst_header_chars[1] * (cat|length) }} .. .. - A bullet item for the {{ cat }} category. .. {% endfor -%} {% else %} - A bullet item for this fragment. EDIT ME! {% endif -%} .. [[[end]]] (checksum: 307b2d307df5eb3a5d316dc850c68011) Literal Prefix -------------- A ``literal:`` prefix means the setting value will be a literal string read from a source file. The setting provides a file name and value name separated by colons:: [scriv] version = literal: myproj/__init__.py: __version__ In this case, the file ``myproj/__init__.py`` will be read, and the ``__version__`` value will be found and used as the version setting. Currently Python, .cfg, TOML, YAML and Cabal files are supported for literals, but other syntaxes can be supported in the future. When reading a literal from a TOML file, the value is specified using periods to separate the sections and key names:: [scriv] version = literal: pyproject.toml: project.version For data from a YAML file, use periods in the value name to access dictionary keys:: [scriv] version = literal: galaxy.yaml: myproduct.versionString When using a Cabal file, the version of the package can be accessed using:: [scriv] version = literal: my-package.cabal: version Commands -------- A ``command:`` prefix indicates that the setting is a shell command to run. The output will be used as the setting:: [scriv] version = command: my_version_tool --next Value Substitution ------------------ The chosen fragment format can be used in settings by referencing ``${config:format}`` in the setting. For example, the default template for new fragments depends on the format because the default setting is:: new_fragment_template = file: new_fragment.${config:format}.j2 Settings ======== These are the specifics about all of the settings read from the configuration file. .. [[[cog import attr import textwrap from scriv.config import _Options fields = sorted(attr.fields(_Options), key=lambda f: f.name) for field in fields: name = field.name print(f"\n\n.. _config_{name}:\n") print(name) print("-" * len(name)) print() text = field.metadata.get("doc", "NO DOC!\n") text = textwrap.dedent(text) print(text) default = field.metadata.get("doc_default") if default is None: default = field.default if isinstance(default, list): default = ", ".join(default) default = f"``{default}``" print("\n".join(textwrap.wrap(f"Default: {default}"))) print() .. ]]] .. _config_categories: categories ---------- Categories to use as headings for changelog items. See :ref:`categories`. Default: ``Removed, Added, Changed, Deprecated, Fixed, Security`` .. _config_changelog: changelog --------- The changelog file managed and read by scriv. The old name for this setting is :ref:`output_file `. Default: ``CHANGELOG.${config:format}`` .. _config_end_marker: end_marker ---------- A marker string indicating where in the changelog file the changelog ends. Default: ``scriv-end-here`` .. _config_entry_title_template: entry_title_template -------------------- The `Jinja`_ template to use for the entry heading text for changelog entries created by ":ref:`cmd_collect`". Default: ``{% if version %}{{ version }} — {% endif %}{{ date.strftime('%Y-%m-%d') }}`` .. _config_format: format ------ The format to use for fragments and for the output changelog file. Can be either "rst" or "md". Default: Derived from the changelog file name if provided, otherwise "rst". .. _config_fragment_directory: fragment_directory ------------------ The directory for fragments. This directory must exist, it will not be created. Default: ``changelog.d`` .. _config_ghrel_template: ghrel_template -------------- The template to use for GitHub releases created by the ``scriv github-release`` command. The extracted Markdown text is available as ``{{body}}``. You must include this to use the text from the changelog file. The version is available as ``{{version}}`` and the title of the entry is available as ``{{title}}``. The data for the release is available in a ``{{release}}`` object, including ``{{release.prerelease}}``. It's a boolean, true if this is a pre-release version. The scriv configuration is available in a ``{{config}}`` object. Default: ``{{body}}`` .. _config_main_branches: main_branches ------------- The branch names considered uninteresting to use in new fragment file names. Default: ``master, main, develop`` .. _config_md_header_level: md_header_level --------------- A number: for Markdown changelog files, this is the heading level to use for the entry heading. Default: ``1`` .. _config_new_fragment_template: new_fragment_template --------------------- The `Jinja`_ template to use for new fragments. Default: ``file: new_fragment.${config:format}.j2`` .. _config_rst_header_chars: rst_header_chars ---------------- Two characters: for reStructuredText changelog files, these are the two underline characters to use. The first is for the heading for each changelog entry, the second is for the category sections within the entry. Default: ``=-`` .. _config_skip_fragments: skip_fragments -------------- A glob pattern for files in the fragment directory that should not be collected. Default: ``README.*`` .. _config_start_marker: start_marker ------------ A marker string indicating where in the changelog file new entries should be inserted. The old name for this setting is :ref:`insert_marker `. Default: ``scriv-insert-here`` .. _config_version: version ------- The string to use as the version number in the next header created by ``scriv collect``. Often, this will be a ``literal:`` directive, to get the version from a string in a source file. Default: (empty) .. [[[end]]] (checksum: 6b03fa55831395ac304313bd43e01ff2) .. _deprecated_config: Deprecated Settings =================== Some names in the config file have been updated. The old names will continue to work, but the new names are preferred: .. [[[cog from scriv.config import DEPRECATED_NAMES print() for old, new in DEPRECATED_NAMES: print(f"- ``{old}`` is now ``{new}``.") print() .. ]]] - ``output_file`` is now ``changelog``. - ``insert_marker`` is now ``start_marker``. .. [[[end]]] (checksum: c0c4c703b20146d23fe0cba53a324d3b) .. _git_settings: Per-User Git Settings ===================== Some aspects of scriv's behavior are configurable for each user rather than for the project as a whole. These settings are read from git. Editing and Adding ------------------ These settings determine whether the ":ref:`cmd_create`" and ":ref:`cmd_collect`" commands will launch an editor, and "git add" the result: - ``scriv.create.edit`` - ``scriv.create.add`` - ``scriv.collect.edit`` - ``scriv.collect.add`` All of these are either "true" or "false", and default to false. You can create these settings with `git config`_ commands, either in the current repo:: $ git config scriv.create.edit true or globally for all of your repos:: $ git config --global scriv.create.edit true User Nickname ------------- Scriv includes your git or GitHub username in the file names of changelog fragments you create. If you don't like the name it finds for you, you can set a name as the ``scriv.user_nick`` git setting. .. _git config: https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration .. include:: include/links.rst scriv-1.7.0/docs/include/000077500000000000000000000000001500123625400152165ustar00rootroot00000000000000scriv-1.7.0/docs/include/links.rst000066400000000000000000000001301500123625400170620ustar00rootroot00000000000000.. Links to be used elsewhere in the docs .. _Jinja: https://jinja.palletsprojects.com scriv-1.7.0/docs/index.rst000066400000000000000000000113561500123625400154420ustar00rootroot00000000000000##### Scriv ##### .. [[[cog import textwrap def include_readme_section(sectname): """Pull a chunk from README.rst""" with open("README.rst") as freadme: for line in freadme: if f".. begin-{sectname}" in line: break for line in freadme: if ".. end" in line: break print(line.rstrip()) .. ]]] .. [[[end]]] (checksum: d41d8cd98f00b204e9800998ecf8427e) Scriv changelog management tool .. [[[cog include_readme_section("badges") ]]] | |pypi-badge| |ci-badge| |coverage-badge| |doc-badge| | |pyversions-badge| |license-badge| | |sponsor-badge| |bluesky-nedbat| |mastodon-nedbat| .. [[[end]]] (checksum: 7fdcea0e3dde536381ddf121907d2b69) Overview ======== Scriv is a command-line tool for helping developers maintain useful changelogs. It manages a directory of changelog fragments. It aggregates them into entries in a CHANGELOG file. Currently scriv implements a simple workflow. The goal is to adapt to more styles of changelog management in the future. Getting Started =============== Scriv writes changelog fragments into a directory called "changelog.d". Start by creating this directory. (By the way, like many aspects of scriv's operation, you can choose a different name for this directory.) To make a new changelog fragment, use the ":ref:`cmd_create`" command. It will make a new file with a filename using the current date and time, your GitHub or Git user name, and your branch name. Changelog fragments should be committed along with all the other changes on your branch. When it is time to release your project, the ":ref:`cmd_collect`" command aggregates all the fragments into a new entry in your changelog file. You can also choose to publish your changelog entries as GitHub releases with the ":ref:`cmd_github_release`" command. It parses the changelog file and creates or updates GitHub releases to match. It can be used even with changelog files that were not created by scriv. .. toctree:: :maxdepth: 1 philosophy concepts commands configuration changelog .. scenarios .. lib, every commit published .. app, no version numbers .. lib, occasional publish How To Contribute ================= `Contributions on GitHub `_ are very welcome. Thanks to all the contributors so far: .. [[[cog include_readme_section("contributors") ]]] | Ned Batchelder | Abhilash Raj | Agustín Piqueres | Alyssa Coughlan | Flo Kuepper | James Gerity | Javier Sagredo | Kurt McKee | Mark Dickinson | Matias Guijarro | Michael Makukha | Rodrigo Girão Serrão | Ronny Pfannschmidt .. [[[end]]] (checksum: 38a9f227f719032ff5b67bcc50bb7276) .. _repo: https://github.com/nedbat/scriv .. [[[cog include_readme_section("badge-links") ]]] .. |pypi-badge| image:: https://img.shields.io/pypi/v/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: PyPI .. |ci-badge| image:: https://github.com/nedbat/scriv/workflows/Test%20Suite/badge.svg :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Build status .. |coverage-badge| image:: https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/nedbat/5a304c1c779d4bcc57be95f847e9327f/raw/covbadge.json :target: https://github.com/nedbat/scriv/actions?query=workflow%3A%22Test+Suite%22 :alt: Coverage .. |doc-badge| image:: https://readthedocs.org/projects/scriv/badge/?version=latest :target: http://scriv.readthedocs.io/en/latest/ :alt: Documentation .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/scriv.svg :target: https://pypi.python.org/pypi/scriv/ :alt: Supported Python versions .. |license-badge| image:: https://img.shields.io/github/license/nedbat/scriv.svg :target: https://github.com/nedbat/scriv/blob/master/LICENSE.txt :alt: License .. |bluesky-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&color=96a3b0&labelColor=3686f7&logo=icloud&logoColor=white&label=@nedbat&url=https%3A%2F%2Fpublic.api.bsky.app%2Fxrpc%2Fapp.bsky.actor.getProfile%3Factor=nedbat.com&query=followersCount :target: https://bsky.app/profile/nedbat.com :alt: nedbat on Bluesky .. |mastodon-nedbat| image:: https://img.shields.io/badge/dynamic/json?style=flat&labelColor=450657&logo=mastodon&logoColor=ffffff&label=@nedbat&query=followers_count&url=https%3A%2F%2Fhachyderm.io%2Fapi%2Fv1%2Faccounts%2Flookup%3Facct=nedbat :target: https://hachyderm.io/@nedbat :alt: nedbat on Mastodon .. |sponsor-badge| image:: https://img.shields.io/badge/%E2%9D%A4-Sponsor%20me-brightgreen?style=flat&logo=GitHub :target: https://github.com/sponsors/nedbat :alt: Sponsor me on GitHub .. [[[end]]] (checksum: 8b12edabfa17d670355a58c750b0a648) scriv-1.7.0/docs/philosophy.rst000066400000000000000000000053061500123625400165270ustar00rootroot00000000000000########## Philosophy ########## .. _philosophy: Scriv's design is guided by a few principles: - Changelogs should be captured in a file in the repository. Scriv writes a CHANGELOG file. - Writing about changes to code should happen close in time to the changes themselves. Scriv encourages writing fragment files to be committed when you commit your code changes. - How you describe a change depends on who you are describing it for. You may need multiple descriptions of the same change. Scriv encourages writing changelog entries directly, rather than copying text from commit messages or pull requests. - The changelog file in the repo should be the source of truth. The information can also be published elsewhere, like GitHub releases. - Different projects have different needs; flexibility is a plus. Scriv doesn't assume any particular issue tracker or packaging system, and allows either .rst or .md files. .. _other_tools: Other Tools =========== Scriv is not the first tool to help manage changelogs, there have been many. None fully embodied scriv's philopsophy. Tools most similar to scriv: - `towncrier`_: built for Twisted, with some unusual specifics: fragment type is the file extension, issue numbers in the file name. Defaults to using ``.rst`` files, but can be configured to produce Markdown or any other output format, provided enough configuration. - `blurb`_: built for CPython development, specific to their workflow: issue numbers from bugs.python.org, only .rst files. - `setuptools-changelog`_: particular to Python projects (uses a setup.py command), and only supports .rst files. - `gitchangelog`_: collects git commit messages into a changelog file. Tools that only read GitHub issues, or only write GitHub releases: - `Chronicler`_: a web hook that watched for merged pull requests, then appends the pull request message to the most recent draft GitHub release. - `fastrelease`_: reads information from GitHub issues, and writes GitHub releases. - `Release Drafter`_: adds text from merged pull requests to the latest draft GitHub release. Other release note tools: - `reno`_: built for Open Stack. It stores changelogs forever as fragment files, only combining for publication. .. _towncrier: https://github.com/hawkowl/towncrier .. _blurb: https://github.com/python/core-workflow/tree/master/blurb .. _setuptools-changelog: https://pypi.org/project/setuptools-changelog/ .. _gitchangelog: https://pypi.org/project/gitchangelog/ .. _fastrelease: https://fastrelease.fast.ai/ .. _Chronicler: https://github.com/NYTimes/Chronicler .. _Release Drafter: https://probot.github.io/apps/release-drafter/ .. _reno: https://docs.openstack.org/reno/latest/user/usage.html scriv-1.7.0/pylintrc000066400000000000000000000143321500123625400144350ustar00rootroot00000000000000[MASTER] ignore = persistent = yes load-plugins = pylint_pytest [MESSAGES CONTROL] enable = blacklisted-name, syntax-error, init-is-generator, return-in-init, function-redefined, not-in-loop, return-outside-function, yield-outside-function, return-arg-in-generator, nonexistent-operator, duplicate-argument-name, abstract-class-instantiated, bad-reversed-sequence, continue-in-finally, method-hidden, access-member-before-definition, no-method-argument, no-self-argument, invalid-slots-object, assigning-non-slot, invalid-slots, inherit-non-class, inconsistent-mro, duplicate-bases, non-iterator-returned, unexpected-special-method-signature, invalid-length-returned, import-error, used-before-assignment, undefined-variable, undefined-all-variable, invalid-all-object, no-name-in-module, unpacking-non-sequence, bad-except-order, raising-bad-type, misplaced-bare-raise, raising-non-exception, catching-non-exception, bad-super-call, no-member, not-callable, assignment-from-no-return, no-value-for-parameter, too-many-function-args, unexpected-keyword-arg, redundant-keyword-arg, invalid-sequence-index, invalid-slice-index, assignment-from-none, not-context-manager, invalid-unary-operand-type, unsupported-binary-operation, repeated-keyword, not-an-iterable, not-a-mapping, unsupported-membership-test, unsubscriptable-object, logging-unsupported-format, logging-too-many-args, logging-too-few-args, bad-format-character, truncated-format-string, format-needs-mapping, missing-format-string-key, too-many-format-args, too-few-format-args, bad-str-strip-call, unreachable, dangerous-default-value, pointless-statement, pointless-string-statement, expression-not-assigned, duplicate-key, confusing-with-statement, using-constant-test, lost-exception, assert-on-tuple, attribute-defined-outside-init, bad-staticmethod-argument, arguments-differ, signature-differs, abstract-method, super-init-not-called, import-self, misplaced-future, global-variable-undefined, redefined-outer-name, redefined-builtin, undefined-loop-variable, cell-var-from-loop, duplicate-except, binary-op-exception, bad-format-string-key, unused-format-string-key, bad-format-string, missing-format-argument-key, unused-format-string-argument, format-combined-specification, missing-format-attribute, invalid-format-index, anomalous-backslash-in-string, anomalous-unicode-escape-in-string, bad-open-mode, boolean-datetime, fatal, astroid-error, parse-error, method-check-failed, raw-checker-failed, empty-docstring, invalid-characters-in-docstring, missing-docstring, wrong-spelling-in-comment, wrong-spelling-in-docstring, unused-import, unused-variable, unused-argument, exec-used, eval-used, bad-classmethod-argument, bad-mcs-classmethod-argument, bad-mcs-method-argument, consider-iterating-dictionary, consider-using-enumerate, multiple-imports, multiple-statements, singleton-comparison, superfluous-parens, unidiomatic-typecheck, unneeded-not, simplifiable-if-statement, no-classmethod-decorator, no-staticmethod-decorator, unnecessary-pass, unnecessary-lambda, useless-else-on-loop, unnecessary-semicolon, reimported, global-variable-not-assigned, global-at-module-level, bare-except, broad-except, logging-not-lazy, redundant-unittest-assert, protected-access, deprecated-module, deprecated-method, too-many-nested-blocks, too-many-statements, too-many-boolean-expressions, wrong-import-order, wrong-import-position, wildcard-import, missing-final-newline, mixed-line-endings, trailing-newlines, trailing-whitespace, unexpected-line-ending-format, bad-option-value, unrecognized-inline-option, useless-suppression, bad-inline-option, deprecated-pragma, disable = invalid-name, line-too-long, file-ignored, bad-indentation, unused-wildcard-import, global-statement, no-else-return, no-else-raise, duplicate-code, fixme, locally-disabled, logging-format-interpolation, logging-fstring-interpolation, suppressed-message, too-few-public-methods, too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, ungrouped-imports, [REPORTS] output-format = text reports = no score = no [BASIC] module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ class-rgx = [A-Z_][a-zA-Z0-9]+$ function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ attr-rgx = [a-z_][a-z0-9_]{2,30}$ argument-rgx = [a-z_][a-z0-9_]{2,30}$ variable-rgx = [a-z_][a-z0-9_]{2,30}$ class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ good-names = f,i,j,k,db,ex,Run,_,__ bad-names = foo,bar,baz,toto,tutu,tata no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ docstring-min-length = 5 [FORMAT] max-line-length = 80 ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ single-line-if-stmt = no max-module-lines = 1000 indent-string = ' ' [MISCELLANEOUS] notes = FIXME,XXX,TODO [SIMILARITIES] min-similarity-lines = 4 ignore-comments = yes ignore-docstrings = yes ignore-imports = no [TYPECHECK] ignore-mixin-members = yes ignored-classes = SQLObject unsafe-load-any-extension = yes generated-members = REQUEST, acl_users, aq_parent, objects, DoesNotExist, can_read, can_write, get_url, size, content, status_code, create, build, fields, tag, org, course, category, name, revision, _meta, [VARIABLES] init-import = no dummy-variables-rgx = _|dummy|unused|.*_unused additional-builtins = [CLASSES] defining-attr-methods = __init__,__new__,setUp valid-classmethod-first-arg = cls valid-metaclass-classmethod-first-arg = mcs [DESIGN] max-args = 5 ignored-argument-names = _.* max-locals = 15 max-returns = 6 max-branches = 12 max-statements = 50 max-parents = 7 max-attributes = 7 min-public-methods = 2 max-public-methods = 20 [IMPORTS] deprecated-modules = regsub,TERMIOS,Bastion,rexec import-graph = ext-import-graph = int-import-graph = [EXCEPTIONS] overgeneral-exceptions = builtins.Exception scriv-1.7.0/pyproject.toml000066400000000000000000000045751500123625400155720ustar00rootroot00000000000000# scriv's pyproject.toml [project] name = "scriv" description = "Scriv changelog management tool" authors = [ {name = "Ned Batchelder", email = "ned@nedbatchelder.com"}, ] license = "Apache-2.0" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "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", ] requires-python = ">= 3.9" dynamic = ["readme", "version", "dependencies"] [project.urls] "Mastodon" = "https://hachyderm.io/@nedbat" "Funding" = "https://github.com/sponsors/nedbat" "Issues" = "https://github.com/nedbat/scriv/issues" "Source" = "https://github.com/nedbat/scriv" "Home" = "https://github.com/nedbat/scriv" "Documentation" = "https://scriv.readthedocs.io" [project.scripts] scriv = "scriv.cli:cli" [project.optional-dependencies] toml = [ 'tomli; python_version < "3.11"' ] yaml = [ "pyyaml" ] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["src"] [tool.setuptools.package-data] scriv = [ "templates/*.*", ] [tool.setuptools.dynamic] version.attr = "scriv.__version__" readme.file = ["README.rst", "CHANGELOG.rst"] dependencies.file = ["requirements/core.txt"] [tool.scriv] ghrel_template = "file: ghrel_template.md.j2" rst_header_chars = "-." version = "literal: src/scriv/__init__.py: __version__" [tool.isort] indent = " " line_length = 80 multi_line_output = 3 include_trailing_comma = true [tool.mypy] python_version = "3.9" show_column_numbers = true show_error_codes = true ignore_missing_imports = true check_untyped_defs = true warn_return_any = true [tool.doc8] max-line-length = 80 [tool.pydocstyle] # D105 = Missing docstring in magic method # D200 = One-line docstring should fit on one line with quotes # D203 = 1 blank line required before class docstring # D212 = Multi-line docstring summary should start at the first line # D406 = Section name should end with a newline (numpy style) # D407 = Missing dashed underline after section (numpy style) # D413 = Missing blank line after last section (numpy style) ignore = ["D105", "D200", "D203", "D212", "D406", "D407", "D413"] scriv-1.7.0/requirements/000077500000000000000000000000001500123625400153665ustar00rootroot00000000000000scriv-1.7.0/requirements/base.in000066400000000000000000000000741500123625400166310ustar00rootroot00000000000000# Core requirements for using this application -r core.txt scriv-1.7.0/requirements/base.txt000066400000000000000000000012311500123625400170360ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # attrs==25.3.0 # via -r requirements/core.txt certifi==2025.1.31 # via requests charset-normalizer==3.4.1 # via requests click==8.1.8 # via # -r requirements/core.txt # click-log click-log==0.4.0 # via -r requirements/core.txt idna==3.10 # via requests jinja2==3.1.6 # via -r requirements/core.txt markdown-it-py==3.0.0 # via -r requirements/core.txt markupsafe==3.0.2 # via jinja2 mdurl==0.1.2 # via markdown-it-py requests==2.32.3 # via -r requirements/core.txt urllib3==2.3.0 # via requests scriv-1.7.0/requirements/constraints.txt000066400000000000000000000007551500123625400205050ustar00rootroot00000000000000# Version constraints for pip-installation. # # This file doesn't install any packages. It specifies version constraints # that will be applied if a package is needed. # # When pinning something here, please provide an explanation of why. Ideally, # link to other information that will help people in the future to remove the # pin when possible. Writing an issue against the offending project and # linking to it here is good. # https://github.com/jazzband/pip-tools/issues/2131 pip==24.2 scriv-1.7.0/requirements/core.txt000066400000000000000000000001521500123625400170550ustar00rootroot00000000000000# Core requirements for using this application attrs click click-log jinja2>=2.7 markdown-it-py requests scriv-1.7.0/requirements/dev.in000066400000000000000000000004751500123625400165020ustar00rootroot00000000000000# Additional requirements for development of this application -r pip-tools.txt # pip-tools and its dependencies, for managing requirements files -r quality.txt # Core and quality check dependencies -r tox.txt # tox and related dependencies build # For kitting scriv-1.7.0/requirements/dev.txt000066400000000000000000000213561500123625400167140ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # alabaster==0.7.16 # via # -r requirements/quality.txt # sphinx astroid==3.3.9 # via # -r requirements/quality.txt # pylint attrs==25.3.0 # via -r requirements/quality.txt babel==2.17.0 # via # -r requirements/quality.txt # sphinx backports-tarfile==1.2.0 # via # -r requirements/quality.txt # jaraco-context black==25.1.0 # via -r requirements/quality.txt build==1.2.2.post1 # via # -r requirements/dev.in # -r requirements/pip-tools.txt # -r requirements/quality.txt # check-manifest # pip-tools cachetools==5.5.2 # via # -r requirements/tox.txt # tox certifi==2025.1.31 # via # -r requirements/quality.txt # requests chardet==5.2.0 # via # -r requirements/tox.txt # tox charset-normalizer==3.4.1 # via # -r requirements/quality.txt # requests check-manifest==0.50 # via -r requirements/quality.txt click==8.1.8 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # black # click-log # pip-tools click-log==0.4.0 # via -r requirements/quality.txt cogapp==3.4.1 # via -r requirements/quality.txt colorama==0.4.6 # via # -r requirements/tox.txt # tox coverage==7.7.1 # via -r requirements/quality.txt dill==0.3.9 # via # -r requirements/quality.txt # pylint distlib==0.3.9 # via # -r requirements/tox.txt # virtualenv doc8==1.1.2 # via -r requirements/quality.txt docutils==0.21.2 # via # -r requirements/quality.txt # doc8 # readme-renderer # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.2.2 # via # -r requirements/quality.txt # pytest filelock==3.18.0 # via # -r requirements/tox.txt # tox # virtualenv freezegun==1.5.1 # via -r requirements/quality.txt id==1.5.0 # via # -r requirements/quality.txt # twine idna==3.10 # via # -r requirements/quality.txt # requests imagesize==1.4.1 # via # -r requirements/quality.txt # sphinx importlib-metadata==8.6.1 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # build # keyring # sphinx # twine iniconfig==2.1.0 # via # -r requirements/quality.txt # pytest isort==6.0.1 # via # -r requirements/quality.txt # pylint jaraco-classes==3.4.0 # via # -r requirements/quality.txt # keyring jaraco-context==6.0.1 # via # -r requirements/quality.txt # keyring jaraco-functools==4.1.0 # via # -r requirements/quality.txt # keyring jedi==0.19.2 # via # -r requirements/quality.txt # pudb jinja2==3.1.6 # via # -r requirements/quality.txt # sphinx keyring==25.6.0 # via # -r requirements/quality.txt # twine markdown-it-py==3.0.0 # via # -r requirements/quality.txt # rich markupsafe==3.0.2 # via # -r requirements/quality.txt # jinja2 mccabe==0.7.0 # via # -r requirements/quality.txt # pylint mdurl==0.1.2 # via # -r requirements/quality.txt # markdown-it-py more-itertools==10.6.0 # via # -r requirements/quality.txt # jaraco-classes # jaraco-functools mypy==1.15.0 # via -r requirements/quality.txt mypy-extensions==1.0.0 # via # -r requirements/quality.txt # black # mypy nh3==0.2.21 # via # -r requirements/quality.txt # readme-renderer packaging==24.2 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # -r requirements/tox.txt # black # build # pudb # pyproject-api # pytest # sphinx # tox # twine parso==0.8.4 # via # -r requirements/quality.txt # jedi pathspec==0.12.1 # via # -r requirements/quality.txt # black pbr==6.1.1 # via # -r requirements/quality.txt # stevedore pip-tools==7.4.1 # via -r requirements/pip-tools.txt platformdirs==4.3.7 # via # -r requirements/quality.txt # -r requirements/tox.txt # black # pylint # tox # virtualenv pluggy==1.5.0 # via # -r requirements/quality.txt # -r requirements/tox.txt # pytest # tox pudb==2024.1.3 # via -r requirements/quality.txt pycodestyle==2.12.1 # via -r requirements/quality.txt pydocstyle==6.3.0 # via -r requirements/quality.txt pygments==2.19.1 # via # -r requirements/quality.txt # doc8 # pudb # readme-renderer # rich # sphinx pylint==3.3.6 # via # -r requirements/quality.txt # pylint-pytest pylint-pytest==1.1.8 # via -r requirements/quality.txt pyproject-api==1.9.0 # via # -r requirements/tox.txt # tox pyproject-hooks==1.2.0 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # build # pip-tools pytest==8.2.0 # via # -r requirements/quality.txt # pylint-pytest # pytest-mock pytest-mock==3.14.0 # via -r requirements/quality.txt python-dateutil==2.9.0.post0 # via # -r requirements/quality.txt # freezegun pyyaml==6.0.2 # via # -r requirements/quality.txt # responses readme-renderer==44.0 # via # -r requirements/quality.txt # twine requests==2.32.3 # via # -r requirements/quality.txt # id # requests-toolbelt # responses # sphinx # twine requests-toolbelt==1.0.0 # via # -r requirements/quality.txt # twine responses==0.25.7 # via -r requirements/quality.txt restructuredtext-lint==1.4.0 # via # -r requirements/quality.txt # doc8 rfc3986==2.0.0 # via # -r requirements/quality.txt # twine rich==13.9.4 # via # -r requirements/quality.txt # twine six==1.17.0 # via # -r requirements/quality.txt # python-dateutil snowballstemmer==2.2.0 # via # -r requirements/quality.txt # pydocstyle # sphinx sphinx==7.4.7 # via # -r requirements/quality.txt # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==3.0.2 # via -r requirements/quality.txt sphinxcontrib-applehelp==2.0.0 # via # -r requirements/quality.txt # sphinx sphinxcontrib-devhelp==2.0.0 # via # -r requirements/quality.txt # sphinx sphinxcontrib-htmlhelp==2.1.0 # via # -r requirements/quality.txt # sphinx sphinxcontrib-jquery==4.1 # via # -r requirements/quality.txt # sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via # -r requirements/quality.txt # sphinx sphinxcontrib-qthelp==2.0.0 # via # -r requirements/quality.txt # sphinx sphinxcontrib-serializinghtml==2.0.0 # via # -r requirements/quality.txt # sphinx stevedore==5.4.1 # via # -r requirements/quality.txt # doc8 tomli==2.2.1 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # -r requirements/tox.txt # black # build # check-manifest # doc8 # mypy # pip-tools # pylint # pyproject-api # pytest # sphinx # tox tomlkit==0.13.2 # via # -r requirements/quality.txt # pylint tox==4.24.2 # via -r requirements/tox.txt twine==6.1.0 # via -r requirements/quality.txt types-freezegun==1.1.10 # via -r requirements/quality.txt types-pyyaml==6.0.12.20241230 # via -r requirements/quality.txt types-requests==2.32.0.20250306 # via -r requirements/quality.txt types-toml==0.10.8.20240310 # via -r requirements/quality.txt typing-extensions==4.12.2 # via # -r requirements/quality.txt # -r requirements/tox.txt # astroid # black # mypy # pylint # rich # tox # urwid urllib3==2.3.0 # via # -r requirements/quality.txt # requests # responses # twine # types-requests urwid==2.6.16 # via # -r requirements/quality.txt # pudb # urwid-readline urwid-readline==0.15.1 # via # -r requirements/quality.txt # pudb virtualenv==20.29.3 # via # -r requirements/tox.txt # tox wcwidth==0.2.13 # via # -r requirements/quality.txt # urwid wheel==0.45.1 # via # -r requirements/pip-tools.txt # pip-tools zipp==3.21.0 # via # -r requirements/pip-tools.txt # -r requirements/quality.txt # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip # setuptools scriv-1.7.0/requirements/doc.in000066400000000000000000000004521500123625400164640ustar00rootroot00000000000000# Requirements for documentation validation -r test.txt # Core and testing dependencies for this package cogapp doc8 # reStructuredText style checker Sphinx # Documentation builder sphinx-rtd-theme # To make it look like readthedocs scriv-1.7.0/requirements/doc.txt000066400000000000000000000103641500123625400167000ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # alabaster==0.7.16 # via sphinx astroid==3.3.9 # via # -r requirements/test.txt # pylint attrs==25.3.0 # via -r requirements/test.txt babel==2.17.0 # via sphinx certifi==2025.1.31 # via # -r requirements/test.txt # requests charset-normalizer==3.4.1 # via # -r requirements/test.txt # requests click==8.1.8 # via # -r requirements/test.txt # click-log click-log==0.4.0 # via -r requirements/test.txt cogapp==3.4.1 # via -r requirements/doc.in coverage==7.7.1 # via -r requirements/test.txt dill==0.3.9 # via # -r requirements/test.txt # pylint doc8==1.1.2 # via -r requirements/doc.in docutils==0.21.2 # via # doc8 # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.2.2 # via # -r requirements/test.txt # pytest freezegun==1.5.1 # via -r requirements/test.txt idna==3.10 # via # -r requirements/test.txt # requests imagesize==1.4.1 # via sphinx importlib-metadata==8.6.1 # via sphinx iniconfig==2.1.0 # via # -r requirements/test.txt # pytest isort==6.0.1 # via # -r requirements/test.txt # pylint jedi==0.19.2 # via # -r requirements/test.txt # pudb jinja2==3.1.6 # via # -r requirements/test.txt # sphinx markdown-it-py==3.0.0 # via -r requirements/test.txt markupsafe==3.0.2 # via # -r requirements/test.txt # jinja2 mccabe==0.7.0 # via # -r requirements/test.txt # pylint mdurl==0.1.2 # via # -r requirements/test.txt # markdown-it-py packaging==24.2 # via # -r requirements/test.txt # pudb # pytest # sphinx parso==0.8.4 # via # -r requirements/test.txt # jedi pbr==6.1.1 # via stevedore platformdirs==4.3.7 # via # -r requirements/test.txt # pylint pluggy==1.5.0 # via # -r requirements/test.txt # pytest pudb==2024.1.3 # via -r requirements/test.txt pygments==2.19.1 # via # -r requirements/test.txt # doc8 # pudb # sphinx pylint==3.3.6 # via # -r requirements/test.txt # pylint-pytest pylint-pytest==1.1.8 # via -r requirements/test.txt pytest==8.2.0 # via # -r requirements/test.txt # pylint-pytest # pytest-mock pytest-mock==3.14.0 # via -r requirements/test.txt python-dateutil==2.9.0.post0 # via # -r requirements/test.txt # freezegun pyyaml==6.0.2 # via # -r requirements/test.txt # responses requests==2.32.3 # via # -r requirements/test.txt # responses # sphinx responses==0.25.7 # via -r requirements/test.txt restructuredtext-lint==1.4.0 # via doc8 six==1.17.0 # via # -r requirements/test.txt # python-dateutil snowballstemmer==2.2.0 # via sphinx sphinx==7.4.7 # via # -r requirements/doc.in # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==3.0.2 # via -r requirements/doc.in sphinxcontrib-applehelp==2.0.0 # via sphinx sphinxcontrib-devhelp==2.0.0 # via sphinx sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx stevedore==5.4.1 # via doc8 tomli==2.2.1 # via # -r requirements/test.txt # doc8 # pylint # pytest # sphinx tomlkit==0.13.2 # via # -r requirements/test.txt # pylint typing-extensions==4.12.2 # via # -r requirements/test.txt # astroid # pylint # urwid urllib3==2.3.0 # via # -r requirements/test.txt # requests # responses urwid==2.6.16 # via # -r requirements/test.txt # pudb # urwid-readline urwid-readline==0.15.1 # via # -r requirements/test.txt # pudb wcwidth==0.2.13 # via # -r requirements/test.txt # urwid zipp==3.21.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools scriv-1.7.0/requirements/pip-tools.in000066400000000000000000000002521500123625400176430ustar00rootroot00000000000000# Just the dependencies to run pip-tools, mainly for the "upgrade" make target pip-tools # Contains pip-compile, used to generate pip requirements files scriv-1.7.0/requirements/pip-tools.txt000066400000000000000000000011371500123625400200570ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # build==1.2.2.post1 # via pip-tools click==8.1.8 # via pip-tools importlib-metadata==8.6.1 # via build packaging==24.2 # via build pip-tools==7.4.1 # via -r requirements/pip-tools.in pyproject-hooks==1.2.0 # via # build # pip-tools tomli==2.2.1 # via # build # pip-tools wheel==0.45.1 # via pip-tools zipp==3.21.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip # setuptools scriv-1.7.0/requirements/quality.in000066400000000000000000000011771500123625400174140ustar00rootroot00000000000000# Requirements for code quality checks -r test.txt # Core and testing dependencies for this package -r doc.txt # Need doc packages for full linting black # Uncompromising code formatting check-manifest # are we packaging files properly? isort # to standardize order of imports mypy # Static type checking pycodestyle # PEP 8 compliance validation pydocstyle # PEP 257 compliance validation pylint twine # For checking distributions types-freezegun types-requests types-toml types-pyyaml scriv-1.7.0/requirements/quality.txt000066400000000000000000000176641500123625400176350ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # alabaster==0.7.16 # via # -r requirements/doc.txt # sphinx astroid==3.3.9 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint attrs==25.3.0 # via # -r requirements/doc.txt # -r requirements/test.txt babel==2.17.0 # via # -r requirements/doc.txt # sphinx backports-tarfile==1.2.0 # via jaraco-context black==25.1.0 # via -r requirements/quality.in build==1.2.2.post1 # via check-manifest certifi==2025.1.31 # via # -r requirements/doc.txt # -r requirements/test.txt # requests charset-normalizer==3.4.1 # via # -r requirements/doc.txt # -r requirements/test.txt # requests check-manifest==0.50 # via -r requirements/quality.in click==8.1.8 # via # -r requirements/doc.txt # -r requirements/test.txt # black # click-log click-log==0.4.0 # via # -r requirements/doc.txt # -r requirements/test.txt cogapp==3.4.1 # via -r requirements/doc.txt coverage==7.7.1 # via # -r requirements/doc.txt # -r requirements/test.txt dill==0.3.9 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint doc8==1.1.2 # via -r requirements/doc.txt docutils==0.21.2 # via # -r requirements/doc.txt # doc8 # readme-renderer # restructuredtext-lint # sphinx # sphinx-rtd-theme exceptiongroup==1.2.2 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest freezegun==1.5.1 # via # -r requirements/doc.txt # -r requirements/test.txt id==1.5.0 # via twine idna==3.10 # via # -r requirements/doc.txt # -r requirements/test.txt # requests imagesize==1.4.1 # via # -r requirements/doc.txt # sphinx importlib-metadata==8.6.1 # via # -r requirements/doc.txt # build # keyring # sphinx # twine iniconfig==2.1.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest isort==6.0.1 # via # -r requirements/doc.txt # -r requirements/quality.in # -r requirements/test.txt # pylint jaraco-classes==3.4.0 # via keyring jaraco-context==6.0.1 # via keyring jaraco-functools==4.1.0 # via keyring jedi==0.19.2 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb jinja2==3.1.6 # via # -r requirements/doc.txt # -r requirements/test.txt # sphinx keyring==25.6.0 # via twine markdown-it-py==3.0.0 # via # -r requirements/doc.txt # -r requirements/test.txt # rich markupsafe==3.0.2 # via # -r requirements/doc.txt # -r requirements/test.txt # jinja2 mccabe==0.7.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint mdurl==0.1.2 # via # -r requirements/doc.txt # -r requirements/test.txt # markdown-it-py more-itertools==10.6.0 # via # jaraco-classes # jaraco-functools mypy==1.15.0 # via -r requirements/quality.in mypy-extensions==1.0.0 # via # black # mypy nh3==0.2.21 # via readme-renderer packaging==24.2 # via # -r requirements/doc.txt # -r requirements/test.txt # black # build # pudb # pytest # sphinx # twine parso==0.8.4 # via # -r requirements/doc.txt # -r requirements/test.txt # jedi pathspec==0.12.1 # via black pbr==6.1.1 # via # -r requirements/doc.txt # stevedore platformdirs==4.3.7 # via # -r requirements/doc.txt # -r requirements/test.txt # black # pylint pluggy==1.5.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pytest pudb==2024.1.3 # via # -r requirements/doc.txt # -r requirements/test.txt pycodestyle==2.12.1 # via -r requirements/quality.in pydocstyle==6.3.0 # via -r requirements/quality.in pygments==2.19.1 # via # -r requirements/doc.txt # -r requirements/test.txt # doc8 # pudb # readme-renderer # rich # sphinx pylint==3.3.6 # via # -r requirements/doc.txt # -r requirements/quality.in # -r requirements/test.txt # pylint-pytest pylint-pytest==1.1.8 # via # -r requirements/doc.txt # -r requirements/test.txt pyproject-hooks==1.2.0 # via build pytest==8.2.0 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint-pytest # pytest-mock pytest-mock==3.14.0 # via # -r requirements/doc.txt # -r requirements/test.txt python-dateutil==2.9.0.post0 # via # -r requirements/doc.txt # -r requirements/test.txt # freezegun pyyaml==6.0.2 # via # -r requirements/doc.txt # -r requirements/test.txt # responses readme-renderer==44.0 # via twine requests==2.32.3 # via # -r requirements/doc.txt # -r requirements/test.txt # id # requests-toolbelt # responses # sphinx # twine requests-toolbelt==1.0.0 # via twine responses==0.25.7 # via # -r requirements/doc.txt # -r requirements/test.txt restructuredtext-lint==1.4.0 # via # -r requirements/doc.txt # doc8 rfc3986==2.0.0 # via twine rich==13.9.4 # via twine six==1.17.0 # via # -r requirements/doc.txt # -r requirements/test.txt # python-dateutil snowballstemmer==2.2.0 # via # -r requirements/doc.txt # pydocstyle # sphinx sphinx==7.4.7 # via # -r requirements/doc.txt # sphinx-rtd-theme # sphinxcontrib-jquery sphinx-rtd-theme==3.0.2 # via -r requirements/doc.txt sphinxcontrib-applehelp==2.0.0 # via # -r requirements/doc.txt # sphinx sphinxcontrib-devhelp==2.0.0 # via # -r requirements/doc.txt # sphinx sphinxcontrib-htmlhelp==2.1.0 # via # -r requirements/doc.txt # sphinx sphinxcontrib-jquery==4.1 # via # -r requirements/doc.txt # sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via # -r requirements/doc.txt # sphinx sphinxcontrib-qthelp==2.0.0 # via # -r requirements/doc.txt # sphinx sphinxcontrib-serializinghtml==2.0.0 # via # -r requirements/doc.txt # sphinx stevedore==5.4.1 # via # -r requirements/doc.txt # doc8 tomli==2.2.1 # via # -r requirements/doc.txt # -r requirements/test.txt # black # build # check-manifest # doc8 # mypy # pylint # pytest # sphinx tomlkit==0.13.2 # via # -r requirements/doc.txt # -r requirements/test.txt # pylint twine==6.1.0 # via -r requirements/quality.in types-freezegun==1.1.10 # via -r requirements/quality.in types-pyyaml==6.0.12.20241230 # via -r requirements/quality.in types-requests==2.32.0.20250306 # via -r requirements/quality.in types-toml==0.10.8.20240310 # via -r requirements/quality.in typing-extensions==4.12.2 # via # -r requirements/doc.txt # -r requirements/test.txt # astroid # black # mypy # pylint # rich # urwid urllib3==2.3.0 # via # -r requirements/doc.txt # -r requirements/test.txt # requests # responses # twine # types-requests urwid==2.6.16 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb # urwid-readline urwid-readline==0.15.1 # via # -r requirements/doc.txt # -r requirements/test.txt # pudb wcwidth==0.2.13 # via # -r requirements/doc.txt # -r requirements/test.txt # urwid zipp==3.21.0 # via # -r requirements/doc.txt # importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools scriv-1.7.0/requirements/test.in000066400000000000000000000006461500123625400167030ustar00rootroot00000000000000# Requirements for test runs. -r base.txt # Core dependencies for this package coverage # for measuring coverage freezegun # for mocking datetime pudb # for when we need to debug pylint-pytest # Understanding of pytest fixtures. pytest pytest-mock # pytest wrapper around mock responses # mock requests pyyaml scriv-1.7.0/requirements/test.txt000066400000000000000000000043311500123625400171070ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # astroid==3.3.9 # via pylint attrs==25.3.0 # via -r requirements/base.txt certifi==2025.1.31 # via # -r requirements/base.txt # requests charset-normalizer==3.4.1 # via # -r requirements/base.txt # requests click==8.1.8 # via # -r requirements/base.txt # click-log click-log==0.4.0 # via -r requirements/base.txt coverage==7.7.1 # via -r requirements/test.in dill==0.3.9 # via pylint exceptiongroup==1.2.2 # via pytest freezegun==1.5.1 # via -r requirements/test.in idna==3.10 # via # -r requirements/base.txt # requests iniconfig==2.1.0 # via pytest isort==6.0.1 # via pylint jedi==0.19.2 # via pudb jinja2==3.1.6 # via -r requirements/base.txt markdown-it-py==3.0.0 # via -r requirements/base.txt markupsafe==3.0.2 # via # -r requirements/base.txt # jinja2 mccabe==0.7.0 # via pylint mdurl==0.1.2 # via # -r requirements/base.txt # markdown-it-py packaging==24.2 # via # pudb # pytest parso==0.8.4 # via jedi platformdirs==4.3.7 # via pylint pluggy==1.5.0 # via pytest pudb==2024.1.3 # via -r requirements/test.in pygments==2.19.1 # via pudb pylint==3.3.6 # via pylint-pytest pylint-pytest==1.1.8 # via -r requirements/test.in pytest==8.2.0 # via # -r requirements/test.in # pylint-pytest # pytest-mock pytest-mock==3.14.0 # via -r requirements/test.in python-dateutil==2.9.0.post0 # via freezegun pyyaml==6.0.2 # via # -r requirements/test.in # responses requests==2.32.3 # via # -r requirements/base.txt # responses responses==0.25.7 # via -r requirements/test.in six==1.17.0 # via python-dateutil tomli==2.2.1 # via # pylint # pytest tomlkit==0.13.2 # via pylint typing-extensions==4.12.2 # via # astroid # pylint # urwid urllib3==2.3.0 # via # -r requirements/base.txt # requests # responses urwid==2.6.16 # via # pudb # urwid-readline urwid-readline==0.15.1 # via pudb wcwidth==0.2.13 # via urwid scriv-1.7.0/requirements/tox.in000066400000000000000000000001351500123625400165270ustar00rootroot00000000000000# Tox and related requirements. tox # Virtualenv management for tests scriv-1.7.0/requirements/tox.txt000066400000000000000000000012211500123625400167350ustar00rootroot00000000000000# # This file is autogenerated by pip-compile with Python 3.9 # by the following command: # # make upgrade # cachetools==5.5.2 # via tox chardet==5.2.0 # via tox colorama==0.4.6 # via tox distlib==0.3.9 # via virtualenv filelock==3.18.0 # via # tox # virtualenv packaging==24.2 # via # pyproject-api # tox platformdirs==4.3.7 # via # tox # virtualenv pluggy==1.5.0 # via tox pyproject-api==1.9.0 # via tox tomli==2.2.1 # via # pyproject-api # tox tox==4.24.2 # via -r requirements/tox.in typing-extensions==4.12.2 # via tox virtualenv==20.29.3 # via tox scriv-1.7.0/src/000077500000000000000000000000001500123625400134325ustar00rootroot00000000000000scriv-1.7.0/src/scriv/000077500000000000000000000000001500123625400145605ustar00rootroot00000000000000scriv-1.7.0/src/scriv/__init__.py000066400000000000000000000001001500123625400166600ustar00rootroot00000000000000""" Scriv changelog management tool. """ __version__ = "1.7.0" scriv-1.7.0/src/scriv/__main__.py000066400000000000000000000001161500123625400166500ustar00rootroot00000000000000"""Enable 'python -m scriv'.""" from .cli import cli cli(prog_name="scriv") scriv-1.7.0/src/scriv/changelog.py000066400000000000000000000074461500123625400170740ustar00rootroot00000000000000"""Changelog and Fragment definitions for Scriv.""" import datetime import logging from pathlib import Path import attr import jinja2 from .config import Config from .format import FormatTools, SectionDict, get_format_tools from .util import partition_lines logger = logging.getLogger(__name__) @attr.s class Fragment: """A changelog fragment.""" path = attr.ib(type=Path) format = attr.ib(type=str, default=None) content = attr.ib(type=str, default=None) def __attrs_post_init__( self, ): # noqa: D105 (Missing docstring in magic method) if self.format is None: self.format = self.path.suffix.lstrip(".") def write(self) -> None: """Write the content to the file.""" self.path.write_text(self.content) def read(self) -> None: """Read the content of the fragment.""" self.content = self.path.read_text() @attr.s class Changelog: """A changelog file.""" path = attr.ib(type=Path) config = attr.ib(type=Config) newline = attr.ib(type=str, default="") text_before = attr.ib(type=str, default="") changelog = attr.ib(type=str, default="") text_after = attr.ib(type=str, default="") def read(self) -> None: """Read the changelog if it exists.""" logger.info(f"Reading changelog {self.path}") if self.path.exists(): with self.path.open("r", encoding="utf-8") as f: changelog_text = f.read() if f.newlines: # .newlines may be None, str, or tuple if isinstance(f.newlines, str): self.newline = f.newlines else: self.newline = f.newlines[0] before, marker, after = partition_lines( changelog_text, self.config.start_marker ) if marker: self.text_before = before + marker rest = after else: self.text_before = "" rest = before self.changelog, marker, after = partition_lines( rest, self.config.end_marker ) self.text_after = marker + after else: logger.warning(f"Changelog {self.path} doesn't exist") def format_tools(self) -> FormatTools: """Get the appropriate FormatTools for this changelog.""" return get_format_tools(self.config.format, self.config) def entry_header(self, version, date=None) -> str: """Format the header for a new entry.""" title_data = { "date": date or datetime.datetime.now(), "version": version, } title_template = jinja2.Template(self.config.entry_title_template) new_title = title_template.render(config=self.config, **title_data) if new_title.strip(): anchor = f"changelog-{version}" if version else None new_header = self.format_tools().format_header( new_title, anchor=anchor ) else: new_header = "" return new_header def entry_text(self, sections: SectionDict) -> str: """Format the text of a new entry.""" return self.format_tools().format_sections(sections) def add_entry(self, header: str, text: str) -> None: """Add a new entry to the top of the changelog.""" self.changelog = header + text + self.changelog def write(self) -> None: """Write the changelog.""" f = self.path.open("w", encoding="utf-8", newline=self.newline or None) with f: f.write(self.text_before) f.write(self.changelog) f.write(self.text_after) def entries(self) -> SectionDict: """Parse the changelog into a SectionDict.""" return self.format_tools().parse_text(self.changelog) scriv-1.7.0/src/scriv/cli.py000066400000000000000000000011531500123625400157010ustar00rootroot00000000000000"""Scriv command-line interface.""" import logging import click import click_log from . import __version__ from .collect import collect from .create import create from .ghrel import github_release from .print import print_ click_log.basic_config(logging.getLogger()) @click.group( help=f"""\ Manage changelogs. https://scriv.readthedocs.io/ Version {__version__} """ ) @click.version_option() def cli() -> None: # noqa: D401 """The main entry point for the scriv command.""" cli.add_command(create) cli.add_command(collect) cli.add_command(github_release) cli.add_command(print_) scriv-1.7.0/src/scriv/collect.py000066400000000000000000000056521500123625400165670ustar00rootroot00000000000000"""Collecting fragments.""" import logging import sys from typing import Optional import click from .gitinfo import git_add, git_config_bool, git_edit, git_rm from .scriv import Scriv from .util import Version, scriv_command logger = logging.getLogger(__name__) @click.command() @click.option( "--add/--no-add", default=None, help="'git add' the updated changelog file and removed fragments.", ) @click.option( "--edit/--no-edit", default=None, help="Open the changelog file in your text editor.", ) @click.option( "--title", default=None, help="The title text to use for this entry." ) @click.option( "--keep", is_flag=True, help="Keep the fragment files that are collected." ) @click.option( "--version", default=None, help="The version name to use for this entry." ) @scriv_command def collect( add: Optional[bool], edit: Optional[bool], title: str, keep: bool, version: str, ) -> None: """ Collect and combine fragments into the changelog. """ if title is not None and version is not None: sys.exit("Can't provide both --title and --version.") if add is None: add = git_config_bool("scriv.collect.add") if edit is None: edit = git_config_bool("scriv.collect.edit") scriv = Scriv() logger.info(f"Collecting from {scriv.config.fragment_directory}") frags = scriv.fragments_to_combine() if not frags: logger.info("No changelog fragments to collect") sys.exit(2) changelog = scriv.changelog() changelog.read() if title is None: version = Version(version or scriv.config.version) if version: # Check that we haven't used this version before. for etitle in changelog.entries().keys(): if etitle is None: continue eversion = Version.from_text(etitle) if eversion is None: sys.exit( f"Entry {etitle!r} is not a valid version! " + "If scriv should ignore this heading, add " + "'scriv-end-here' somewhere before it." ) if eversion == version: sys.exit( f"Entry {etitle!r} already uses " + f"version {str(version)!r}." ) new_header = changelog.entry_header(version=version) else: new_header = changelog.format_tools().format_header(title) new_text = changelog.entry_text(scriv.combine_fragments(frags)) changelog.add_entry(new_header, new_text) changelog.write() if edit: git_edit(changelog.path) if add: git_add(changelog.path) if not keep: for frag in frags: logger.info(f"Deleting fragment file {str(frag.path)!r}") if add: git_rm(frag.path) else: frag.path.unlink() scriv-1.7.0/src/scriv/config.py000066400000000000000000000372551500123625400164130ustar00rootroot00000000000000"""Scriv configuration.""" from __future__ import annotations import configparser import contextlib import logging import pkgutil import re from pathlib import Path from typing import Any import attr from .exceptions import ScrivException from .literals import find_literal from .optional import tomllib from .shell import run_shell_command logger = logging.getLogger(__name__) DEFAULT_FORMAT = "rst" DEFAULT_CHANGELOG = "CHANGELOG.${config:format}" @attr.s class _Options: """ All the settable options for Scriv. """ # The directory for fragments waiting to be collected. Also can have # templates and settings for scriv. fragment_directory = attr.ib( type=str, default="changelog.d", metadata={ "doc": """\ The directory for fragments. This directory must exist, it will not be created. """, }, ) # What format for fragments? reStructuredText ("rst") or Markdown ("md"). format = attr.ib( # type: ignore[assignment] type=str, default=None, validator=attr.validators.optional(attr.validators.in_(["rst", "md"])), metadata={ "doc": """\ The format to use for fragments and for the output changelog file. Can be either "rst" or "md". """, "doc_default": f"""\ Derived from the changelog file name if provided, otherwise "{DEFAULT_FORMAT}". """, }, ) # The categories for changelog fragments. Can be empty for no # categorization. categories = attr.ib( type=list, default=[ "Removed", "Added", "Changed", "Deprecated", "Fixed", "Security", ], metadata={ "doc": """\ Categories to use as headings for changelog items. See :ref:`categories`. """, }, ) changelog = attr.ib( type=str, default=None, metadata={ "doc": """\ The changelog file managed and read by scriv. The old name for this setting is :ref:`output_file `. """, "doc_default": f"``{DEFAULT_CHANGELOG}``", }, ) start_marker = attr.ib( type=str, default="scriv-insert-here", metadata={ "doc": """\ A marker string indicating where in the changelog file new entries should be inserted. The old name for this setting is :ref:`insert_marker `. """, }, ) end_marker = attr.ib( type=str, default="scriv-end-here", metadata={ "doc": """\ A marker string indicating where in the changelog file the changelog ends. """, }, ) # The characters to use for header and section underlines in rst files. rst_header_chars = attr.ib( type=str, default="=-", validator=attr.validators.matches_re(r"\S\S"), metadata={ "doc": """\ Two characters: for reStructuredText changelog files, these are the two underline characters to use. The first is for the heading for each changelog entry, the second is for the category sections within the entry. """, }, ) # What header level to use for markdown changelog entries? md_header_level = attr.ib( type=str, default="1", validator=attr.validators.matches_re(r"[123456]"), converter=attr.converters.optional(str), metadata={ "doc": """\ A number: for Markdown changelog files, this is the heading level to use for the entry heading. """, }, ) # The name of the template for new fragments. new_fragment_template = attr.ib( type=str, default="file: new_fragment.${config:format}.j2", metadata={ "doc": """\ The `Jinja`_ template to use for new fragments. """, }, ) # The template for the title of the changelog entry. entry_title_template = attr.ib( type=str, default=( "{% if version %}{{ version }} — {% endif %}" + "{{ date.strftime('%Y-%m-%d') }}" ), metadata={ "doc": """\ The `Jinja`_ template to use for the entry heading text for changelog entries created by ":ref:`cmd_collect`". """, }, ) # The version string to include in the title if wanted. version = attr.ib( type=str, default="", metadata={ "doc": """\ The string to use as the version number in the next header created by ``scriv collect``. Often, this will be a ``literal:`` directive, to get the version from a string in a source file. """, "doc_default": "(empty)", }, ) # Branches that aren't interesting enough to use in fragment file names. main_branches = attr.ib( type=list, default=["master", "main", "develop"], metadata={ "doc": """\ The branch names considered uninteresting to use in new fragment file names. """, }, ) # Glob for files in the fragments directory that should not be collected. skip_fragments = attr.ib( type=str, default="README.*", metadata={ "doc": """\ A glob pattern for files in the fragment directory that should not be collected. """, }, ) # Template for GitHub releases ghrel_template = attr.ib( type=str, default="{{body}}", metadata={ "doc": """\ The template to use for GitHub releases created by the ``scriv github-release`` command. The extracted Markdown text is available as ``{{body}}``. You must include this to use the text from the changelog file. The version is available as ``{{version}}`` and the title of the entry is available as ``{{title}}``. The data for the release is available in a ``{{release}}`` object, including ``{{release.prerelease}}``. It's a boolean, true if this is a pre-release version. The scriv configuration is available in a ``{{config}}`` object. """, }, ) def post_create(self): """ Reconcile some interdependent settings after creating the object. """ if self.format is None: if self.changelog is not None and "${" not in self.changelog: self.format = Path(self.changelog).suffix[1:] else: self.format = DEFAULT_FORMAT if self.changelog is None: self.changelog = DEFAULT_CHANGELOG # Map of old config names to new config names. DEPRECATED_NAMES = [ ("output_file", "changelog"), ("insert_marker", "start_marker"), ] @contextlib.contextmanager def validator_exceptions(): """ Context manager for attrs operations that validate. Attrs >= 22 says ValueError will have a bunch of arguments, and we only want to see the first, and raised as ScrivException. """ try: yield except ValueError as ve: raise ScrivException(f"Invalid configuration: {ve.args[0]}") from ve class Config: """ Configuration for Scriv. All the settable options for Scriv, with resolution of values within other values. """ def __init__(self, post_create_=True, **kwargs): """All values in _Options can be set as keywords.""" with validator_exceptions(): self._options = _Options(**kwargs) if post_create_: self._options.post_create() def __getattr__(self, name): """Proxy to self._options, and resolve the value.""" fields = attr.fields_dict(_Options) if name not in fields: raise AttributeError(f"Scriv configuration has no {name!r} option") attrdef = fields[name] value = getattr(self._options, name) if attrdef.type is list: if isinstance(value, str): value = convert_list(value) else: try: value = self.resolve_value(value) except ScrivException as se: raise ScrivException( f"Couldn't read {name!r} setting: {se}" ) from se setattr(self, name, value) return value @classmethod def read(cls) -> Config: """ Read the configuration to use. Configuration will be read from setup.cfg, tox.ini, or changelog.d/scriv.ini. If setup.cfg or tox.ini defines a new fragment_directory, then scriv.ini is read from there. The section can be named ``[scriv]`` or ``[tool.scriv]``. """ config = cls(post_create_=False) config.read_one_config("setup.cfg") config.read_one_config("tox.ini") config.read_one_toml("pyproject.toml") config.read_one_config( str(Path(config.fragment_directory) / "scriv.ini") ) with validator_exceptions(): attr.validate(config._options) config._options.post_create() return config def get_set_option(self, scriv_data, config_name, opt_name): """ Set one option from a config file setting. """ try: val: Any = scriv_data[config_name] except KeyError: pass else: attrdef = attr.fields_dict(_Options)[opt_name] if callable(attrdef.converter): val = attrdef.converter(val) setattr(self._options, opt_name, val) def read_one_config(self, configfile: str) -> None: """ Read one configuration file, adding values to `self`. """ logger.debug(f"Looking for config file {configfile}") parser = configparser.ConfigParser() files_read = parser.read(configfile) if not files_read: logger.debug(f"{configfile} doesn't exist") return logger.debug(f"{configfile} was read") section_name = next( ( name for name in ["scriv", "tool.scriv"] if parser.has_section(name) ), None, ) if section_name: scriv_data = parser[section_name] for old, new in DEPRECATED_NAMES: self.get_set_option(scriv_data, old, new) for attrdef in attr.fields(_Options): self.get_set_option(scriv_data, attrdef.name, attrdef.name) def read_one_toml(self, tomlfile: str) -> None: """ Read one .toml file if it exists, adding values to `self`. """ logger.debug(f"Looking for config file {tomlfile}") tomlpath = Path(tomlfile) if not tomlpath.exists(): logger.debug(f"{tomlfile} doesn't exist") return toml_text = tomlpath.read_text(encoding="utf-8") logger.debug(f"{tomlfile} was read") if tomllib is None: # Toml support isn't installed. Only print an exception if the # config file seems to have settings for us. has_scriv = re.search(r"(?m)^\[tool\.scriv\]", toml_text) if has_scriv: msg = ( "Can't read {!r} without TOML support. " + "Install with [toml] extra" ).format(tomlfile) raise ScrivException(msg) else: # We have toml installed, parse the file and look for our settings. data = tomllib.loads(toml_text) try: scriv_data = data["tool"]["scriv"] except KeyError: # No settings for us return for old, new in DEPRECATED_NAMES: self.get_set_option(scriv_data, old, new) for attrdef in attr.fields(_Options): self.get_set_option(scriv_data, attrdef.name, attrdef.name) def resolve_value(self, value: str) -> str: """ Interpret prefixes in config files to find the actual value. Also, "${config:format}" is replaced with the configured format ("rst" or "md"). Prefixes: "file:" read the content from a file. "literal:" read a literal string from a file. "command:" read the output of a shell command. """ if self._options.format is not None: value = value.replace("${config:format}", self._options.format) if value.startswith("file:"): file_name = value.partition(":")[2].strip() value = self.read_file_value(file_name) elif value.startswith("literal:"): try: _, file_name, literal_name = value.split(":", maxsplit=2) except ValueError as ve: raise ScrivException(f"Missing value name: {value!r}") from ve file_name = file_name.strip() if not file_name: raise ScrivException(f"Missing file name: {value!r}") literal_name = literal_name.strip() if not literal_name: raise ScrivException(f"Missing value name: {value!r}") try: found = find_literal(file_name, literal_name) except Exception as exc: raise ScrivException( f"Couldn't find literal {value!r}: {exc}" ) from exc if found is None: raise ScrivException( f"Couldn't find literal {literal_name!r} in {file_name}: " + f"{value!r}" ) value = found elif value.startswith("command:"): cmd = value.partition(":")[2].strip() ok, out = run_shell_command(cmd) if not ok: raise ScrivException(f"Command {cmd!r} failed:\n{out}") if out.count("\n") == 1: out = out.rstrip("\r\n") value = out return value def read_file_value(self, file_name: str) -> str: """ Find the value of a setting that has been specified as a file name. """ value = None possibilities = [] if not re.match(r"\.\.?[/\\]", file_name): possibilities.append(Path(self.fragment_directory) / file_name) possibilities.append(Path(".") / file_name) for file_path in possibilities: if file_path.exists(): value = file_path.read_text() break else: # No path, and doesn't exist: try it as a built-in. try: file_bytes = pkgutil.get_data("scriv", "templates/" + file_name) except OSError: pass else: assert file_bytes value = file_bytes.decode("utf-8") if value is None: raise ScrivException(f"No such file: {file_name}") return value def convert_list(val: str) -> list[str]: """ Convert a string value from a config into a list of strings. Elements can be separated by commas or newlines. """ vals = re.split(r"[\n,]", val) vals = [v.strip() for v in vals] vals = [v for v in vals if v] return vals scriv-1.7.0/src/scriv/create.py000066400000000000000000000026161500123625400164020ustar00rootroot00000000000000"""Creating fragments.""" import logging import sys from typing import Optional import click from .gitinfo import git_add, git_config_bool, git_edit from .scriv import Scriv from .util import scriv_command logger = logging.getLogger(__name__) @click.command() @click.option( "--add/--no-add", default=None, help="'git add' the created file." ) @click.option( "--edit/--no-edit", default=None, help="Open the created file in your text editor.", ) @scriv_command def create(add: Optional[bool], edit: Optional[bool]) -> None: """ Create a new changelog fragment. """ if add is None: add = git_config_bool("scriv.create.add") if edit is None: edit = git_config_bool("scriv.create.edit") scriv = Scriv() frag = scriv.new_fragment() file_path = frag.path if not file_path.parent.exists(): sys.exit( f"Output directory {str(file_path.parent)!r} doesn't exist," + " please create it." ) if file_path.exists(): sys.exit(f"File {file_path} already exists, not overwriting") logger.info(f"Creating {file_path}") frag.write() if edit: git_edit(file_path) sections = scriv.sections_from_fragment(frag) if not sections: logger.info("Empty fragment, aborting...") file_path.unlink() sys.exit() if add: git_add(file_path) scriv-1.7.0/src/scriv/exceptions.py000066400000000000000000000001641500123625400173140ustar00rootroot00000000000000"""Specialized exceptions for scriv.""" class ScrivException(Exception): """Any exception raised by scriv.""" scriv-1.7.0/src/scriv/format.py000066400000000000000000000040671500123625400164310ustar00rootroot00000000000000"""Dispatcher for format-based knowledge.""" import abc from typing import Optional from .config import Config # When collecting changelog fragments, we group them by their category into # Sections. A SectionDict maps category names to a list of the paragraphs in # that section. For projects not using categories, the key will be None. SectionDict = dict[Optional[str], list[str]] class FormatTools(abc.ABC): """Methods and data about specific formats.""" def __init__(self, config: Optional[Config] = None): """Create a FormatTools with the specified configuration.""" self.config = config or Config() @abc.abstractmethod def parse_text(self, text: str) -> SectionDict: """ Parse text to find sections. Args: text: the marked-up text. Returns: A dict mapping section headers to a list of the paragraphs in each section. """ @abc.abstractmethod def format_header(self, text: str, anchor: Optional[str] = None) -> str: """ Format the header for a new changelog entry. """ @abc.abstractmethod def format_sections(self, sections: SectionDict) -> str: """ Format a series of sections into marked-up text. """ @abc.abstractmethod def convert_to_markdown( self, text: str, name: str = "", fail_if_warn: bool = False ) -> str: """ Convert this format to Markdown. """ def get_format_tools(fmt: str, config: Config) -> FormatTools: """ Return the FormatTools to use. Args: fmt: One of the supported formats ("rst" or "md"). config: The configuration settings to use. """ if fmt == "rst": from . import ( # pylint: disable=cyclic-import,import-outside-toplevel format_rst, ) return format_rst.RstTools(config) else: assert fmt == "md" from . import ( # pylint: disable=cyclic-import,import-outside-toplevel format_md, ) return format_md.MdTools(config) scriv-1.7.0/src/scriv/format_md.py000066400000000000000000000063401500123625400171050ustar00rootroot00000000000000"""Markdown text knowledge for scriv.""" import re from typing import Optional from .format import FormatTools, SectionDict class MdTools(FormatTools): """Specifics about how to work with Markdown.""" def parse_text( self, text ) -> SectionDict: # noqa: D102 (inherited docstring) lines = text.splitlines() # If there's an insert marker, start there. for lineno, line in enumerate(lines): if self.config.start_marker in line: lines = lines[lineno + 1 :] break sections: SectionDict = {} in_comment = False paragraphs = None section_mark = None for line in lines: line = line.rstrip() if in_comment: if re.search(r"-->$", line): in_comment = False else: if re.search(r"^\s*$", line): # A one-line comment, skip it. continue if re.search(r"""^$""", line): # An anchor, we don't need those. continue if re.search(r"^\s* {% for cat in config.categories -%} {% endfor -%} scriv-1.7.0/src/scriv/templates/new_fragment.rst.j2000066400000000000000000000006671500123625400223070ustar00rootroot00000000000000.. A new scriv changelog fragment. {% if config.categories -%} .. .. Uncomment the section that is right (remove the leading dots). .. For top level release notes, leave all the headers commented out. .. {% for cat in config.categories -%} .. {{ cat }} .. {{ config.rst_header_chars[1] * (cat|length) }} .. .. - A bullet item for the {{ cat }} category. .. {% endfor -%} {% else %} - A bullet item for this fragment. EDIT ME! {% endif -%} scriv-1.7.0/src/scriv/util.py000066400000000000000000000066241500123625400161170ustar00rootroot00000000000000"""Miscellanous helpers.""" from __future__ import annotations import collections import functools import logging import re import sys from collections.abc import Sequence from typing import TypeVar import click_log from .exceptions import ScrivException T = TypeVar("T") K = TypeVar("K") def order_dict( d: dict[K | None, T], keys: Sequence[K | None] ) -> dict[K | None, T]: """ Produce an OrderedDict of `d`, but with the keys in `keys` order. Keys in `d` that don't appear in `keys` will be at the end in an undetermined order. """ with_order = collections.OrderedDict() to_insert = set(d) for k in keys: if k not in to_insert: continue with_order[k] = d[k] to_insert.remove(k) for k in to_insert: with_order[k] = d[k] return with_order def partition_lines(text: str, marker: str) -> tuple[str, str, str]: """ Split `text` by lines, similar to str.partition. The splitting line is the first line containing `marker`. """ lines = text.splitlines(keepends=True) marker_pos = [i for i, line in enumerate(lines) if marker in line] if not marker_pos: return (text, "", "") pos = marker_pos[0] return ( "".join(lines[:pos]), lines[pos], "".join(lines[pos + 1 :]), ) VERSION_REGEX = r"""(?ix) # based on https://peps.python.org/pep-0440/ \b # at a word boundary v? # maybe a leading "v" (\d+!)? # maybe a version epoch \d+(\.\d+)+ # the meat of the version number: N.N.N (?P
        [-._]?[a-z]+\.?\d*
    )?                      # maybe a pre-release: .beta3
    ([-._][a-z]+\d*)*       # maybe post and dev releases
    (\+\w[\w.]*\w)?         # maybe a local version
    \b
    """


class Version:
    """
    A version string that compares correctly.

    For example, "v1.2.3" and "1.2.3" are considered the same.

    """

    def __init__(self, vtext: str) -> None:
        """Create a smart version from a string version number."""
        self.vtext = vtext

    def __repr__(self):
        return f""

    def __str__(self):
        return self.vtext

    def __bool__(self):
        return bool(self.vtext)

    def __eq__(self, other):
        assert isinstance(other, Version)
        return self.vtext.lstrip("v") == other.vtext.lstrip("v")

    def __hash__(self):
        return hash(self.vtext.lstrip("v"))

    @classmethod
    def from_text(cls, text: str) -> Version | None:
        """Find a version number in a text string."""
        m = re.search(VERSION_REGEX, text)
        if m:
            return cls(m[0])
        return None

    def is_prerelease(self) -> bool:  # noqa: D400
        """Is this version number a pre-release?"""
        m = re.fullmatch(VERSION_REGEX, self.vtext)
        assert m  # the version must be a valid version
        return bool(m["pre"])


def scriv_command(func):
    """
    Decorate scriv commands to provide scriv-specific behavior.

    - ScrivExceptions don't show tracebacks

    - Set the log level from the command line for all of scriv.

    """

    @functools.wraps(func)
    @click_log.simple_verbosity_option(logging.getLogger("scriv"))
    def _wrapped(*args, **kwargs):
        try:
            func(*args, **kwargs)
        except ScrivException as exc:
            sys.exit(str(exc))

    return _wrapped
scriv-1.7.0/tests/000077500000000000000000000000001500123625400140055ustar00rootroot00000000000000scriv-1.7.0/tests/__init__.py000066400000000000000000000000331500123625400161120ustar00rootroot00000000000000"""The tests for scriv."""
scriv-1.7.0/tests/conftest.py000066400000000000000000000050641500123625400162110ustar00rootroot00000000000000"""Fixture definitions."""

import os
import sys
import traceback
from collections.abc import Iterable
from pathlib import Path

import pytest
import responses
from click.testing import CliRunner

# We want to be able to test scriv without any extras installed.  But responses
# installs PyYaml.  If we are testing the no-extras scenario, then: after we've
# imported responses above, and before we import any scriv modules below,
# clobber the yaml module so that scriv's import will fail, simulating PyYaml
# not being available.
if os.getenv("SCRIV_TEST_NO_EXTRAS", ""):
    sys.modules["yaml"] = None  # type: ignore[assignment]

# pylint: disable=wrong-import-position

from scriv.cli import cli as scriv_cli

from .faker import FakeGit, FakeRunCommand

# Pytest will rewrite assertions in test modules, but not elsewhere.
# This tells pytest to also rewrite assertions in these files:
pytest.register_assert_rewrite("tests.helpers")


@pytest.fixture()
def fake_run_command(mocker):
    """Replace gitinfo.run_command with a fake."""
    return FakeRunCommand(mocker)


@pytest.fixture()
def fake_git(fake_run_command) -> FakeGit:
    """Get a FakeGit to use in tests."""
    return FakeGit(fake_run_command)


@pytest.fixture()
def temp_dir(tmpdir) -> Iterable[Path]:
    """Make and change into the tmpdir directory, as a Path."""
    old_dir = os.getcwd()
    tmpdir.chdir()
    try:
        yield Path(str(tmpdir))
    finally:
        os.chdir(old_dir)


@pytest.fixture()
def cli_invoke(temp_dir: Path):
    """
    Produce a function to invoke the Scriv cli with click.CliRunner.

    The test will run in a temp directory.
    """

    def invoke(command, expect_ok=True):
        runner = CliRunner(mix_stderr=False)
        result = runner.invoke(scriv_cli, command)
        print(result.stdout, end="")
        print(result.stderr, end="", file=sys.stderr)
        if result.exception:
            traceback.print_exception(
                None, result.exception, result.exception.__traceback__
            )
        if expect_ok:
            assert result.exception is None
            assert result.exit_code == 0
        return result

    return invoke


@pytest.fixture()
def changelog_d(temp_dir: Path) -> Path:
    """Make a changelog.d directory, and return a Path() to it."""
    the_changelog_d = temp_dir / "changelog.d"
    the_changelog_d.mkdir()
    return the_changelog_d


@pytest.fixture(autouse=True, name="responses")
def no_http_requests():
    """Activate `responses` for all tests, so no real HTTP happens."""
    with responses.RequestsMock() as rsps:
        yield rsps
scriv-1.7.0/tests/faker.py000066400000000000000000000100741500123625400154510ustar00rootroot00000000000000"""Fake implementations of some of our external information sources."""

import re
import shlex
from collections.abc import Iterable
from typing import Callable, Optional

from scriv.shell import CmdResult

# A function that simulates run_command.
CmdHandler = Callable[[list[str]], CmdResult]


# A regex to help with catching some (but not all) invalid Git config keys.
WORD = r"[a-zA-Z][a-zA-Z0-9-]*"
GIT_CONFIG_KEY = rf"{WORD}(\..*)?\.{WORD}"


class FakeRunCommand:
    """
    A fake implementation of run_command.

    Add handlers for commands with `add_handler`.
    """

    def __init__(self, mocker):
        """Make the faker."""
        self.handlers: dict[str, CmdHandler] = {}
        self.mocker = mocker
        self.patch_module("scriv.shell")

    def patch_module(self, mod_name: str) -> None:
        """Replace ``run_command`` in `mod_name` with our fake."""
        self.mocker.patch(f"{mod_name}.run_command", self)

    def add_handler(self, argv0: str, handler: CmdHandler) -> None:
        """
        Add a handler for a command.

        The first word of the command is `argv0`.  The handler will be called
        with the complete argv list.  It must return the same results that
        `run_command` would have returned.
        """
        self.handlers[argv0] = handler

    def __call__(self, cmd: str) -> CmdResult:
        """Do the faking!."""
        argv = shlex.split(cmd)
        if argv[0] in self.handlers:
            return self.handlers[argv[0]](argv)
        return (False, f"no fake command handler: {argv}")


class FakeGit:
    """Simulate aspects of our local Git."""

    def __init__(self, frc: FakeRunCommand) -> None:
        """Make a FakeGit from a FakeRunCommand."""
        # Initialize with basic defaults.
        self.config: dict[str, str] = {
            "core.bare": "false",
            "core.repositoryformatversion": "0",
        }
        self.branch = "main"
        self.editor = "vi"
        self.tags: set[str] = set()
        self.remotes: dict[str, tuple[str, str]] = {}

        # Hook up our run_command handler.
        frc.add_handler("git", self.run_command)

    def run_command(self, argv: list[str]) -> CmdResult:
        """Simulate git commands."""
        # todo: match/case someday
        if argv[1] == "config":
            if argv[2] == "--get":
                if argv[3] in self.config:
                    return (True, self.config[argv[3]] + "\n")
                else:
                    return (False, f"error: no such key: {argv[3]}")
        elif argv[1:] == ["rev-parse", "--abbrev-ref", "HEAD"]:
            return (True, self.branch + "\n")
        elif argv[1:] == ["tag"]:
            return (True, "".join(tag + "\n" for tag in self.tags))
        elif argv[1:] == ["var", "GIT_EDITOR"]:
            return (True, self.editor + "\n")
        elif argv[1:] == ["remote", "-v"]:
            out = []
            for name, (url, push_url) in self.remotes.items():
                out.append(f"{name}\t{url} (fetch)\n")
                out.append(f"{name}\t{push_url} (push)\n")
            return (True, "".join(out))
        return (False, f"no fake git command: {argv}")

    def set_config(self, name: str, value: str) -> None:
        """Set a fake Git configuration value."""
        if re.fullmatch(GIT_CONFIG_KEY, name) is None:
            raise ValueError(f"invalid key: {name!r}")
        self.config[name] = value

    def set_branch(self, branch_name: str) -> None:
        """Set the current fake branch."""
        self.branch = branch_name

    def set_editor(self, editor_name: str) -> None:
        """Set the name of the fake editor Git will launch."""
        self.editor = editor_name

    def add_tags(self, tags: Iterable[str]) -> None:
        """Add tags to the repo."""
        self.tags.update(tags)

    def add_remote(
        self, name: str, url: str, push_url: Optional[str] = None
    ) -> None:
        """Add a remote with a name and a url."""
        self.remotes[name] = (url, push_url or url)

    def remove_remote(self, name: str) -> None:
        """Remove the remote `name`."""
        del self.remotes[name]
scriv-1.7.0/tests/helpers.py000066400000000000000000000015471500123625400160300ustar00rootroot00000000000000"""Testing helpers."""

from unittest import mock


def without_module(using_module, missing_module_name: str):
    """
    Hide a module for testing.

    Use this in a test function to make an optional module unavailable during
    the test::

        with without_module(scriv.something, 'toml'):
            use_toml_somehow()

    Arguments:
        using_module: a module in which to hide `missing_module_name`.
        missing_module_name: the name of the module to hide.

    """
    return mock.patch.object(using_module, missing_module_name, None)


def check_logs(caplog, expected):
    """
    Compare log records from caplog.

    Only caplog records from a logger mentioned in expected are considered.
    """
    logger_names = {r[0] for r in expected}
    records = [r for r in caplog.record_tuples if r[0] in logger_names]
    assert records == expected
scriv-1.7.0/tests/test_changelog.py000066400000000000000000000025231500123625400173470ustar00rootroot00000000000000"""Tests of scriv/changelog.py"""

import pytest

from scriv.changelog import Changelog
from scriv.config import Config

A = """\
Hello
Goodbye
"""

B = """\
Now
more than
ever.
"""

BODY = """\
2022-09-13
==========

Added
-----

- Wrote tests for Changelog.

2022-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Launching missiles no longer targets ourselves.

- Typos corrected.
"""

BODY_SECTIONS = {
    "2022-09-13": [
        "Added\n-----",
        "- Wrote tests for Changelog.",
    ],
    "2022-02-25": [
        "Added\n-----",
        "- Now you can send email with this tool.",
        "Fixed\n-----",
        "- Launching missiles no longer targets ourselves.",
        "- Typos corrected.",
    ],
}


@pytest.mark.parametrize(
    "text",
    [
        BODY,
        ".. INSERT\n" + BODY,
        A + "(INSERT)\n" + BODY,
        A + "INSERT\n" + BODY + ".. END\n",
        A + ".. INSERT\n" + BODY + "(END)\n" + B,
        BODY + ".. END\n",
        BODY + ".. END\n" + B,
    ],
)
def test_round_trip(text, temp_dir):
    path = temp_dir / "foo.rst"
    config = Config(start_marker="INSERT", end_marker="END")
    path.write_text(text)
    changelog = Changelog(path, config)
    changelog.read()
    assert changelog.entries() == BODY_SECTIONS
    changelog.write()
    assert path.read_text() == text
scriv-1.7.0/tests/test_collect.py000066400000000000000000000444301500123625400170500ustar00rootroot00000000000000"""Test collection logic."""

import textwrap
from unittest.mock import call

import freezegun
import pytest

COMMENT = """\
.. this line should be dropped

"""

COMMENT_MD = """\

"""

FRAG1 = """\
Fixed
-----

- Launching missiles no longer targets ourselves.
"""

FRAG2 = """\
Added
-----

- Now you can send email with this tool.

Fixed
-----

- Typos corrected.
"""

FRAG2_MD = """\
# Added

- Now you can send email with this tool.

# Fixed

- Typos corrected.
"""

FRAG3 = """\
Obsolete
--------

- This section has the wrong name.
"""

CHANGELOG_1_2 = """\

2020-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Launching missiles no longer targets ourselves.

- Typos corrected.
"""

CHANGELOG_2_1_3 = """\

2020-02-25
==========

Added
-----

- Now you can send email with this tool.

Fixed
-----

- Typos corrected.

- Launching missiles no longer targets ourselves.

Obsolete
--------

- This section has the wrong name.
"""

MARKED_CHANGELOG_A = """\
================
My Great Project
================

Blah blah.

Changes
=======

.. scriv-insert-here
"""

UNMARKED_CHANGELOG_B = """\

Other stuff
===========

Blah blah.
"""

CHANGELOG_HEADER = """\

2020-02-25
==========
"""


def test_collect_simple(cli_invoke, changelog_d, temp_dir):
    # Sections are ordered by the config file.
    # Fragments in sections are in time order.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "README.rst").write_text("This directory has fragments")
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG2)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We didn't use --keep, so the files should be gone.
    assert (changelog_d / "scriv.ini").exists()
    assert not (changelog_d / "20170616_nedbat.rst").exists()
    assert not (changelog_d / "20170617_nedbat.rst").exists()


def test_collect_ordering(cli_invoke, changelog_d, temp_dir):
    # Fragments in sections are in time order.
    # Unknown sections come after the known ones.
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG2)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_collect_mixed_format(cli_invoke, changelog_d, temp_dir):
    # Fragments can be in mixed formats.
    (changelog_d / "README.md").write_text("Don't take this file.")
    (changelog_d / "20170616_nedbat.md").write_text(COMMENT_MD + FRAG2_MD)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_collect_inserts_at_marker(cli_invoke, changelog_d, temp_dir):
    # Collected text is inserted into CHANGELOG where marked.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(MARKED_CHANGELOG_A + UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = (
        MARKED_CHANGELOG_A
        + CHANGELOG_HEADER
        + "\n"
        + FRAG1
        + UNMARKED_CHANGELOG_B
    )
    assert changelog_text == expected


def test_collect_inserts_at_marker_no_header(cli_invoke, changelog_d, temp_dir):
    # No title this time.
    (changelog_d / "scriv.ini").write_text("[scriv]\nentry_title_template =\n")
    # Collected text is inserted into CHANGELOG where marked.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(MARKED_CHANGELOG_A + UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = MARKED_CHANGELOG_A + "\n" + FRAG1 + UNMARKED_CHANGELOG_B
    assert changelog_text == expected


def test_collect_prepends_if_no_marker(cli_invoke, changelog_d, temp_dir):
    # Collected text is inserted at the top of CHANGELOG if no marker.
    changelog = temp_dir / "CHANGELOG.rst"
    changelog.write_text(UNMARKED_CHANGELOG_B)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = CHANGELOG_HEADER + "\n" + FRAG1 + UNMARKED_CHANGELOG_B
    assert changelog_text == expected


def test_collect_keep(cli_invoke, changelog_d, temp_dir):
    # --keep tells collect to not delete the fragment files.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--keep"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --keep, so the collected files should still exist.
    assert (changelog_d / "scriv.ini").exists()
    assert (changelog_d / "20170616_nedbat.rst").exists()
    assert (changelog_d / "20170617_nedbat.rst").exists()


def test_collect_no_categories(cli_invoke, changelog_d, temp_dir):
    # Categories can be empty, making a simpler changelog.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "scriv.ini").write_text("[scriv]\ncategories=\n")
    (changelog_d / "20170616_nedbat.rst").write_text("- The first change.\n")
    (changelog_d / "20170617_nedbat.rst").write_text(
        COMMENT + "- The second change.\n"
    )
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = (
        "\n"
        + "2020-02-25\n"
        + "==========\n\n"
        + "- The first change.\n\n"
        + "- The second change.\n"
    )
    assert changelog_text == expected


def test_collect_uncategorized_fragments(cli_invoke, changelog_d, temp_dir):
    # If using categories, even uncategorized fragments will be collected.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text("- The second change.\n")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text()
    expected = "\n2020-02-25\n==========\n\n- The second change.\n\n" + FRAG1
    assert changelog_text == expected


def test_collect_add(mocker, cli_invoke, changelog_d, temp_dir):
    # --add tells collect to tell git what's going on.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_call = mocker.patch("subprocess.call")
    mock_call.return_value = 0
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--add"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --add, so the collected files were git rm'd
    assert mock_call.mock_calls == [
        call(["git", "add", "CHANGELOG.rst"]),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170616_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170617_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
    ]


def test_collect_add_rm_fail(mocker, cli_invoke, changelog_d, temp_dir):
    # --add, but fail to remove a file.
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_call = mocker.patch("subprocess.call")
    mock_call.side_effect = [0, 99]
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        result = cli_invoke(["collect", "--add"], expect_ok=False)
    assert result.exit_code == 99
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    # We used --add, so the collected files were git rm'd
    assert mock_call.mock_calls == [
        call(["git", "add", "CHANGELOG.rst"]),
        call(
            [
                "git",
                "rm",
                str(
                    (changelog_d / "20170616_nedbat.rst").relative_to(temp_dir)
                ),
            ]
        ),
    ]


def test_collect_edit(fake_git, mocker, cli_invoke, changelog_d, temp_dir):
    # --edit tells collect to open the changelog in an editor.
    fake_git.set_editor("my_fav_editor")
    (changelog_d / "scriv.ini").write_text("# this shouldn't be collected\n")
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    mock_edit = mocker.patch("click.edit")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect", "--edit"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_1_2
    mock_edit.assert_called_once_with(
        filename="CHANGELOG.rst", editor="my_fav_editor"
    )


def test_collect_version_in_config(cli_invoke, changelog_d, temp_dir):
    # The version number to use in the changelog entry can be specified in the
    # config file.
    changelog = temp_dir / "CHANGELOG.rst"
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = v12.34b\n")
    (changelog_d / "20170616_nedbat.rst").write_text("- The first change.\n")
    with freezegun.freeze_time("2020-02-26T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = changelog.read_text(encoding="utf-8")
    expected = (
        "\n"
        + ".. _changelog-v12.34b:\n"
        + "\n"
        + "v12.34b — 2020-02-26\n"
        + "====================\n\n"
        + "- The first change.\n"
    )
    assert changelog_text == expected


@pytest.mark.parametrize(
    "platform, newline",
    (
        ("Windows", "\r\n"),
        ("Linux", "\n"),
        ("Mac", "\r"),
    ),
)
def test_collect_respect_existing_newlines(
    cli_invoke,
    changelog_d,
    temp_dir,
    platform,
    newline,
):
    """Verify that existing newline styles are preserved during collection."""

    index_map = {
        "\r\n": 0,
        "\n": 1,
        "\r": 2,
    }

    changelog = temp_dir / "CHANGELOG.rst"
    existing_text = "Line one" + newline + "Line two"
    with changelog.open("wb") as file:
        file.write(existing_text.encode("utf8"))
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    with changelog.open("rb") as file:
        new_text = file.read().decode("utf8")

    counts = [new_text.count("\r\n")]  # Windows
    counts.append(new_text.count("\n") - counts[0])  # Linux
    counts.append(new_text.count("\r") - counts[0])  # Mac

    msg = platform + " newlines were not preserved"
    assert counts.pop(index_map[newline]), msg
    assert counts == [0, 0], msg


def test_no_newlines(cli_invoke, changelog_d, temp_dir):
    changelog = temp_dir / "CHANGELOG.rst"
    with changelog.open("wb") as file:
        file.write(b"no newline")
    (changelog_d / "20170616_nedbat.rst").write_text("A bare line")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    new_text = changelog.read_text()
    assert new_text == "\n2020-02-25\n==========\n\nA bare line\nno newline"


def test_mixed_newlines(cli_invoke, changelog_d, temp_dir):
    changelog = temp_dir / "CHANGELOG.rst"
    with changelog.open("wb") as file:
        file.write(b"slashr\rslashn\n")
    (changelog_d / "20170616_nedbat.rst").write_text("A bare line")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    new_text = changelog.read_text()
    assert (
        new_text == "\n2020-02-25\n==========\n\nA bare line\nslashr\nslashn\n"
    )


def test_configure_skipped_fragments(cli_invoke, changelog_d, temp_dir):
    # The skipped "readme" files can be configured.
    (changelog_d / "scriv.ini").write_text("[scriv]\nskip_fragments = ALL*\n")
    (changelog_d / "ALLABOUT.md").write_text("Don't take this file.")
    (changelog_d / "20170616_nedbat.md").write_text(COMMENT_MD + FRAG2_MD)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG1)
    (changelog_d / "20170618_joedev.rst").write_text(COMMENT + FRAG3)
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["collect"])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == CHANGELOG_2_1_3


def test_no_fragments(cli_invoke, changelog_d, temp_dir, caplog):
    (changelog_d / "README.rst").write_text("This directory has fragments")
    (temp_dir / "CHANGELOG.rst").write_text("Not much\n")
    with freezegun.freeze_time("2020-02-25T15:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 2
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    assert changelog_text == "Not much\n"
    assert "No changelog fragments to collect" in caplog.text


def test_title_provided(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "20170616_nedbat.rst").write_text(COMMENT + FRAG1 + COMMENT)
    (changelog_d / "20170617_nedbat.rst").write_text(COMMENT + FRAG2)
    title = "This is the Header"
    cli_invoke(["collect", "--title", title])
    changelog_text = (temp_dir / "CHANGELOG.rst").read_text()
    # With --title provided, the first header is literally what was provided.
    lines = CHANGELOG_1_2.splitlines()
    lines[1] = title
    lines[2] = len(title) * "="
    assert changelog_text == "\n".join(lines) + "\n"


def test_title_and_version_clash(cli_invoke):
    result = cli_invoke(
        ["collect", "--title", "xx", "--version", "1.2"], expect_ok=False
    )
    assert result.exit_code == 1
    assert str(result.exception) == "Can't provide both --title and --version."


def test_duplicate_version(cli_invoke, changelog_d, temp_dir):
    (temp_dir / "foob.py").write_text(
        """# comment\n__version__ = "12.34.56"\n"""
    )
    (changelog_d / "scriv.ini").write_text(
        "[scriv]\nversion = literal:foob.py: __version__\n"
    )
    (changelog_d / "20170616_nedbat.rst").write_text(FRAG1)
    with freezegun.freeze_time("2022-09-18T15:18:19"):
        cli_invoke(["collect"])

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry '12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_duplicate_version_2(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            12.34.57 — 2022-09-19
            =====================

            A quick fix.

            12.34.56 — 2022-09-18
            =====================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry '12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_duplicate_version_with_v(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            v12.34.57 — 2022-09-19
            ======================

            A quick fix.

            v12.34.56 — 2022-09-18
            ======================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert (
        str(result.exception)
        == "Entry 'v12.34.56 — 2022-09-18' already uses version '12.34.56'."
    )


def test_unparsable_version(cli_invoke, changelog_d, temp_dir):
    (changelog_d / "scriv.ini").write_text("[scriv]\nversion = 12.34.56\n")
    (temp_dir / "CHANGELOG.rst").write_text(
        textwrap.dedent(
            """\
            Preamble that doesn't count

            v12.34.57 — 2022-09-19
            ======================

            A quick fix.

            Not a version at all.
            =====================

            Good stuff.
            """
        ),
        encoding="utf-8",
    )

    # Make a new fragment, and collect again without changing the version.
    (changelog_d / "20170617_nedbat.rst").write_text(FRAG2)
    with freezegun.freeze_time("2022-09-18T16:18:19"):
        result = cli_invoke(["collect"], expect_ok=False)
    assert result.exit_code == 1
    assert str(result.exception) == (
        "Entry 'Not a version at all.' is not a valid version! "
        + "If scriv should ignore this heading, add "
        + "'scriv-end-here' somewhere before it."
    )
scriv-1.7.0/tests/test_config.py000066400000000000000000000327661500123625400167010ustar00rootroot00000000000000"""Tests of scriv/config.py"""

import re
import textwrap

import pytest

import scriv.config
from scriv.config import Config
from scriv.exceptions import ScrivException
from scriv.optional import tomllib

from .helpers import without_module

CONFIG1 = """\
[scriv]
changelog = README.md
categories = New, Different, Gone, Bad
start_marker = FIRST!
"""

OLD_CONFIG1 = CONFIG1.replace("changelog = ", "output_file = ").replace(
    "start_marker = ", "insert_marker = "
)

CONFIG2 = """\
[someotherthing]
no_idea = what this is

[tool.scriv]
changelog = README.md
categories =
    New
    Different
    Gone
    Bad

[more stuff]
value = 17
"""

GENERIC_TOML_CONFIG = """\
[project]
name = "spam"
version = "2020.0.0"
description = "Lovely Spam! Wonderful Spam!"
readme = "README.rst"
requires-python = ">=3.9"
license = {file = "LICENSE.txt"}
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
authors = [
  {email = "hi@pradyunsg.me"},
  {name = "Tzu-Ping Chung"}
]
maintainers = [
  {name = "Brett Cannon", email = "brett@python.org"}
]
classifiers = [
  "Development Status :: 4 - Beta",
  "Programming Language :: Python"
]
"""

TOML_CONFIG = (
    GENERIC_TOML_CONFIG
    + """
[tool.scriv]
changelog = "README.md"
categories = [
    "New",
    "Different",
    "Gone",
    "Bad",
]
start_marker = "FIRST!"
# other scriv options

["more stuff"]
value = 17
"""
)

OLD_TOML_CONFIG = TOML_CONFIG.replace("changelog = ", "output_file = ").replace(
    "start_marker = ", "insert_marker = "
)


def test_defaults(temp_dir):
    # No configuration files anywhere, just get all the defaults.
    config = Config.read()
    assert config.fragment_directory == "changelog.d"
    assert config.format == "rst"
    assert config.new_fragment_template.startswith(
        ".. A new scriv changelog fragment"
    )
    assert config.categories == [
        "Removed",
        "Added",
        "Changed",
        "Deprecated",
        "Fixed",
        "Security",
    ]
    assert config.changelog == "CHANGELOG.rst"
    assert config.start_marker == "scriv-insert-here"
    assert config.rst_header_chars == "=-"
    assert config.md_header_level == "1"
    assert "{{ date.strftime('%Y-%m-%d') }}" in config.entry_title_template
    assert config.main_branches == ["master", "main", "develop"]
    assert config.skip_fragments == "README.*"
    assert config.version == ""


@pytest.mark.parametrize("config_text", [CONFIG1, OLD_CONFIG1])
def test_reading_config(config_text, temp_dir):
    (temp_dir / "setup.cfg").write_text(config_text)
    config = Config.read()
    assert config.fragment_directory == "changelog.d"
    assert config.changelog == "README.md"
    assert config.format == "md"
    assert config.categories == ["New", "Different", "Gone", "Bad"]
    assert config.start_marker == "FIRST!"


def test_reading_config_list(temp_dir):
    (temp_dir / "tox.ini").write_text(CONFIG2)
    config = Config.read()
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_reading_config_from_directory(changelog_d):
    # The settings file can be changelog.d/scriv.ini .
    (changelog_d / "scriv.ini").write_text(CONFIG1)
    config = Config.read()
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_reading_config_from_other_directory(temp_dir):
    # setup.cfg can set the fragment directory, and then scriv.ini will
    # be found there.
    (temp_dir / "scriv.d").mkdir()
    (temp_dir / "scriv.d" / "scriv.ini").write_text(CONFIG1)
    (temp_dir / "setup.cfg").write_text(
        "[tool.scriv]\nfragment_directory = scriv.d\n"
    )
    config = Config.read()
    assert config.fragment_directory == "scriv.d"
    assert config.categories == ["New", "Different", "Gone", "Bad"]


def test_unknown_option(temp_dir):
    config = Config.read()
    expected = "Scriv configuration has no 'foo' option"
    with pytest.raises(AttributeError, match=expected):
        _ = config.foo


def test_custom_template(changelog_d):
    # You can define your own template with your own name.
    (changelog_d / "start_here.j2").write_text("Custom template.")
    fmt = Config(
        new_fragment_template="file: start_here.j2"
    ).new_fragment_template
    assert fmt == "Custom template."


def test_file_with_dots(temp_dir, changelog_d):
    # A file: spec with dot components is relative to the current directory.
    (changelog_d / "start_here.j2").write_text("The wrong one")
    (temp_dir / "start_here.j2").write_text("The right one")
    fmt = Config(
        new_fragment_template="file: ./start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_file_with_path_search_order(temp_dir, changelog_d):
    # A file: spec with path components is relative to the changelog directory
    # and then the current directory.
    (changelog_d / "files").mkdir()
    (changelog_d / "files" / "start_here.j2").write_text("The right one")
    (temp_dir / "files").mkdir()
    (temp_dir / "files" / "start_here.j2").write_text("The wrong one")
    fmt = Config(
        new_fragment_template="file: files/start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_file_with_path_only_current_dir(temp_dir, changelog_d):
    # A file: spec with path components is relative to the changelog directory
    # and then the current directory.
    (temp_dir / "files").mkdir()
    (temp_dir / "files" / "start_here.j2").write_text("The right one")
    fmt = Config(
        new_fragment_template="file: files/start_here.j2"
    ).new_fragment_template
    assert fmt == "The right one"


def test_missing_file_with_path(temp_dir, changelog_d):
    # A file: spec with path components is relative to the current directory.
    (changelog_d / "start_here.j2").write_text("The wrong one")
    msg = (
        r"Couldn't read 'new_fragment_template' setting: "
        + r"No such file: there[/\\]start_here.j2"
    )
    with pytest.raises(ScrivException, match=msg):
        config = Config(new_fragment_template="file: there/start_here.j2")
        _ = config.new_fragment_template


def test_unknown_format():
    with pytest.raises(
        ScrivException,
        match=r"'format' must be in \['rst', 'md'\] \(got 'xyzzy'\)",
    ):
        Config(format="xyzzy")


def test_no_such_template():
    # If you specify a template name, and it doesn't exist, an error will
    # be raised.
    msg = (
        r"Couldn't read 'new_fragment_template' setting: "
        + r"No such file: foo\.j2"
    )
    with pytest.raises(ScrivException, match=msg):
        config = Config(new_fragment_template="file: foo.j2")
        _ = config.new_fragment_template


def test_override_default_name(changelog_d):
    # You can define a file named new_fragment.rst.j2, and it will be read
    # as the template.
    (changelog_d / "new_fragment.rst.j2").write_text("Hello there!")
    fmt = Config().new_fragment_template
    assert fmt == "Hello there!"


def test_file_reading(changelog_d):
    # Any setting can be read from a file, even where it doesn't make sense.
    (changelog_d / "hello.txt").write_text("Xyzzy")
    text = Config(changelog="file:hello.txt").changelog
    assert text == "Xyzzy"


def test_literal_reading(temp_dir):
    # Any setting can be read from a literal in a file.
    (temp_dir / "sub").mkdir()
    (temp_dir / "sub" / "foob.py").write_text(
        """# comment\n__version__ = "12.34.56"\n"""
    )
    text = Config(version="literal:sub/foob.py: __version__").version
    assert text == "12.34.56"


@pytest.mark.parametrize(
    "bad_spec, msg_rx",
    [
        (
            "literal: myproj.py",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing value name: 'literal: myproj.py'"
            ),
        ),
        (
            "literal: myproj.py:",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing value name: 'literal: myproj.py:'"
            ),
        ),
        (
            "literal: myproj.py:  version",
            (
                r"Couldn't read 'version' setting: "
                + r"Couldn't find literal 'version' in myproj.py: "
                + r"'literal: myproj.py:  version'"
            ),
        ),
        (
            "literal: : version",
            (
                r"Couldn't read 'version' setting: "
                + r"Missing file name: 'literal: : version'"
            ),
        ),
        (
            "literal: no_file.py: version",
            (
                r"Couldn't read 'version' setting: "
                + r"Couldn't find literal 'literal: no_file.py: version': "
                + r".* 'no_file.py'"
            ),
        ),
    ],
)
def test_bad_literal_spec(bad_spec, msg_rx, temp_dir):
    (temp_dir / "myproj.py").write_text("""nothing_to_see_here = 'hello'\n""")
    with pytest.raises(ScrivException, match=msg_rx):
        config = Config(version=bad_spec)
        _ = config.version


@pytest.mark.parametrize("chars", ["", "#", "#=-", "# ", "  "])
def test_rst_chars_is_two_chars(chars):
    # rst_header_chars must be exactly two non-space characters.
    msg = rf"Invalid configuration: 'rst_header_chars' must match.*'{chars}'"
    with pytest.raises(ScrivException, match=msg):
        Config(rst_header_chars=chars)


def test_md_format(changelog_d):
    (changelog_d / "scriv.ini").write_text("[scriv]\nformat = md\n")
    config = Config.read()
    assert config.changelog == "CHANGELOG.md"
    template = re.sub(r"\s+", " ", config.new_fragment_template)
    assert template.startswith("

            # Added

            - This thing was added.
              And we liked it.

            

            - Another thing we added.

            
            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Another thing we added.",
                ]
            },
            id="comments_ignored",
        ),
        # Multiple section headers.
        pytest.param(
            """\
            # Added

            - This thing was added.
              And we liked it.


            # Fixed

            - This thing was fixed.

            - Another thing was fixed.

            # Added

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers",
        ),
        # Multiple section headers at a different level.
        pytest.param(
            """\
            ### Added

            - This thing was added.
              And we liked it.


            ###     Fixed or Something

            - This thing was fixed.

            - Another thing was fixed.

            ### Added

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed or Something": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers_2",
        ),
        # It's fine to have no header at all.
        pytest.param(
            """\
            - No header at all.
            """,
            {None: ["- No header at all."]},
            id="no_header",
        ),
        # It's fine to have comments with no header, and multiple bulllets.
        pytest.param(
            """\
            

            - No header at all.

            - Just plain bullets.
            """,
            {None: ["- No header at all.", "- Just plain bullets."]},
            id="no_header_2",
        ),
        # A file with only comments and blanks will produce nothing.
        pytest.param(
            """\
            

            


            """,
            {},
            id="empty",
        ),
        # Multiple levels of headings only splits on the top-most one.
        pytest.param(
            """\
            # The big title

            Ignore this stuff

            

            (prelude)

            
            ## Section one

            ### subhead

            In the sub

            ### subhead 2

            Also sub

            
            ## Section two

            In section two.

            ### subhead 3
            s2s3
            """,
            {
                None: ["(prelude)"],
                "Section one": [
                    "### subhead",
                    "In the sub",
                    "### subhead 2",
                    "Also sub",
                ],
                "Section two": [
                    "In section two.",
                    "### subhead 3\ns2s3",
                ],
            },
            id="multilevel",
        ),
    ],
)
def test_parse_text(text, parsed):
    actual = MdTools().parse_text(textwrap.dedent(text))
    assert actual == parsed


@pytest.mark.parametrize(
    "sections, expected",
    [
        pytest.param(
            [
                (
                    "Added",
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
                (
                    "Fixed",
                    ["- This thing was fixed.", "- Another thing was fixed."],
                ),
            ],
            """\

            ### Added

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.

            ### Fixed

            - This thing was fixed.

            - Another thing was fixed.
            """,
            id="one",
        ),
        pytest.param(
            [
                (
                    None,
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
            ],
            """\

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.
            """,
            id="two",
        ),
    ],
)
def test_format_sections(sections, expected):
    sections = collections.OrderedDict(sections)
    actual = MdTools(Config(md_header_level="2")).format_sections(sections)
    assert actual == textwrap.dedent(expected)


@pytest.mark.parametrize(
    "config_kwargs, text, fh_kwargs, result",
    [
        ({}, "2020-07-26", {}, "\n# 2020-07-26\n"),
        ({"md_header_level": "3"}, "2020-07-26", {}, "\n### 2020-07-26\n"),
        (
            {},
            "2022-04-03",
            {"anchor": "here"},
            "\n\n# 2022-04-03\n",
        ),
    ],
)
def test_format_header(config_kwargs, text, fh_kwargs, result):
    actual = MdTools(Config(**config_kwargs)).format_header(text, **fh_kwargs)
    assert actual == result


def test_convert_to_markdown():
    # Markdown's convert_to_markdown is a no-op.
    md = "# Nonsense\ndoesn't matter\n- whatever ``more ** junk---"
    converted = MdTools().convert_to_markdown(md)
    assert converted == md
scriv-1.7.0/tests/test_format_rst.py000066400000000000000000000275671500123625400176170ustar00rootroot00000000000000"""Tests for scriv/format_rst.py."""

import collections
import re
import textwrap

import pytest

from scriv.config import Config
from scriv.exceptions import ScrivException
from scriv.format_rst import RstTools


@pytest.mark.parametrize(
    "text, parsed",
    [
        # Comments are ignored, and the section headers found.
        pytest.param(
            """\
            .. Comments can be here
            .. and here.
            ..
            .. and here.
            Added
            -----

            - This thing was added.
              And we liked it.

            .. More comments can be here
            ..
            .. And here.

            """,
            {"Added": ["- This thing was added.\n  And we liked it."]},
            id="comments_ignored",
        ),
        # Multiple section headers.
        pytest.param(
            """\
            Added
            -----

            - This thing was added.
              And we liked it.


            Fixed
            -----

            - This thing was fixed.

            - Another thing was fixed.

            Added
            -----

            - Also added
              this thing
              that is very important.

            """,
            {
                "Added": [
                    "- This thing was added.\n  And we liked it.",
                    "- Also added\n  this thing\n  that is very important.",
                ],
                "Fixed": [
                    "- This thing was fixed.",
                    "- Another thing was fixed.",
                ],
            },
            id="multiple_headers",
        ),
        # The specific character used for the header line is unimportant.
        pytest.param(
            """\
            Added
            ^^^^^
            - This thing was added.

            Fixed
            ^^^^^
            - This thing was fixed.
            """,
            {
                "Added": ["- This thing was added."],
                "Fixed": ["- This thing was fixed."],
            },
            id="different_underlines",
        ),
        # You can even use periods as the underline, it won't be confused for a
        # comment.
        pytest.param(
            """\
            Fixed
            .....
            - This thing was fixed.

            Added
            .....

            .. a comment.

            - This thing was added.
            """,
            {
                "Added": ["- This thing was added."],
                "Fixed": ["- This thing was fixed."],
            },
            id="period_underline",
        ),
        # It's fine to have no header at all.
        pytest.param(
            """\
            - No header at all.
            """,
            {None: ["- No header at all."]},
            id="no_header",
        ),
        # It's fine to have comments with no header, and multiple bulllets.
        pytest.param(
            """\
            .. This is a scriv fragment.

            - No header at all.

            - Just plain bullets.
            """,
            {None: ["- No header at all.", "- Just plain bullets."]},
            id="no_header_2",
        ),
        # RST syntax is intricate. We understand a subset of it.
        pytest.param(
            """\
            .. _fixed.1:

            Fixed
            .....
            - This thing was fixed: `issue 42`_.

            .. _issue 42: https://github.com/thing/issue/42

            .. _added:

            Added
            .....

            .. a comment.

            - This thing was added.

            .. note::
                This thing doesn't work yet.
                Not sure it ever will... :(

            .. [cite] A citation

            .. |subst| image:: substitution.png

            ..

            """,
            {
                "Added": [
                    "- This thing was added.",
                    (
                        ".. note::\n"
                        + "    This thing doesn't work yet.\n"
                        + "    Not sure it ever will... :("
                    ),
                    ".. [cite] A citation",
                    ".. |subst| image:: substitution.png",
                ],
                "Fixed": [
                    "- This thing was fixed: `issue 42`_.",
                    ".. _issue 42: https://github.com/thing/issue/42",
                ],
            },
            id="intricate_syntax",
        ),
        # A file with only comments and blanks will produce nothing.
        pytest.param(
            """\
            .. Nothing to see here.
            ..

            .. Or here.


            """,
            {},
            id="empty",
        ),
        # Multiple levels of headings only splits on the top-most one.
        pytest.param(
            """\
            =====
            TITLE
            =====

            Irrelevant stuff

            Heading
            =======

            Ignore this

            .. scriv-insert-here

            (prelude)

            ===========
            Section one
            ===========

            subhead
            -------

            In the sub

            subhead 2
            ---------

            Also sub

            Section two
            ===========

            In section two.

            subhead 3
            ---------
            s2s3
            """,
            {
                None: ["(prelude)"],
                "Section one": [
                    "subhead\n-------",
                    "In the sub",
                    "subhead 2\n---------",
                    "Also sub",
                ],
                "Section two": [
                    "In section two.",
                    "subhead 3\n---------\ns2s3",
                ],
            },
            id="multilevel",
        ),
    ],
)
def test_parse_text(text, parsed):
    actual = RstTools().parse_text(textwrap.dedent(text))
    assert actual == parsed


@pytest.mark.parametrize(
    "sections, expected",
    [
        pytest.param(
            [
                (
                    "Added",
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
                (
                    "Fixed",
                    ["- This thing was fixed.", "- Another thing was fixed."],
                ),
            ],
            """\

            Added
            ~~~~~

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.

            Fixed
            ~~~~~

            - This thing was fixed.

            - Another thing was fixed.
            """,
            id="one",
        ),
        pytest.param(
            [
                (
                    None,
                    [
                        "- This thing was added.\n  And we liked it.",
                        "- Also added\n  this thing\n  that is very important.",
                    ],
                ),
            ],
            """\

            - This thing was added.
              And we liked it.

            - Also added
              this thing
              that is very important.
            """,
            id="two",
        ),
    ],
)
def test_format_sections(sections, expected):
    sections = collections.OrderedDict(sections)
    actual = RstTools(Config(rst_header_chars="#~")).format_sections(sections)
    assert actual == textwrap.dedent(expected)


@pytest.mark.parametrize(
    "config_kwargs, text, fh_kwargs, result",
    [
        ({}, "2020-07-26", {}, "\n2020-07-26\n==========\n"),
        (
            {"rst_header_chars": "*-"},
            "2020-07-26",
            {},
            "\n2020-07-26\n**********\n",
        ),
        (
            {},
            "2022-04-03",
            {"anchor": "here"},
            "\n.. _here:\n\n2022-04-03\n==========\n",
        ),
    ],
)
def test_format_header(config_kwargs, text, fh_kwargs, result):
    actual = RstTools(Config(**config_kwargs)).format_header(text, **fh_kwargs)
    assert actual == result


@pytest.mark.parametrize("fail_if_warn", [False, True])
def test_fake_pandoc(fake_run_command, fail_if_warn):
    fake_run_command.patch_module("scriv.format_rst")
    expected_args = [
        "pandoc",
        "-frst",
        "-tmarkdown_strict",
        "--markdown-headings=atx",
        "--wrap=none",
    ]
    if fail_if_warn:
        expected_args.append("--fail-if-warnings")
    expected_text = "The converted text!\nis multi-line\n"

    def fake_pandoc(argv):
        # We got the arguments we expected, plus one more.
        assert argv[:-1] == expected_args
        return (True, expected_text)

    fake_run_command.add_handler("pandoc", fake_pandoc)
    assert (
        RstTools().convert_to_markdown("Hello", fail_if_warn=fail_if_warn)
        == expected_text
    )


def test_fake_pandoc_failing(fake_run_command):
    fake_run_command.patch_module("scriv.format_rst")
    error_text = "There was a problem!!?!"

    def fake_pandoc(argv):  # pylint: disable=unused-argument
        return (False, error_text)

    fake_run_command.add_handler("pandoc", fake_pandoc)
    expected = f"Couldn't convert ReST to Markdown in '':\n{error_text}"
    with pytest.raises(ScrivException, match=re.escape(expected)):
        _ = RstTools().convert_to_markdown("Hello")


@pytest.mark.parametrize(
    "rst_text, md_text",
    [
        (
            """\
            Look at this list:

            - One issue fixed: `issue 123`_.

            - One change merged: `Big change `_.

            - Improved the `home page `_.

            - One more `small change`__.

            .. _issue 123: https://github.com/joe/project/issues/123
            .. _pull 234: https://github.com/joe/project/pull/234
            __ https://github.com/joe/project/issues/999
            """,
            """\
            Look at this list:

            - One issue fixed: [issue 123](https://github.com/joe/project/issues/123).
            - One change merged: [Big change](https://github.com/joe/project/pull/234).
            - Improved the [home page](https://example.com/homepage).
            - One more [small change](https://github.com/joe/project/issues/999).
            """,
        ),
    ],
)
def test_convert_to_markdown(rst_text, md_text):
    converted = RstTools().convert_to_markdown(textwrap.dedent(rst_text))
    # Different versions of pandoc produce slightly different results.  But the
    # markdown is rendered the same regardless of spaces after the
    # bullet-hyphens, so fix them.
    converted = re.sub(r"(?m)^-\s+", "- ", converted)
    expected = textwrap.dedent(md_text)
    assert expected == converted


@pytest.mark.parametrize(
    "rst_text, msg",
    [
        # Various styles of broken links:
        (
            "One issue fixed: `issue 123`_.",
            "Reference not found for 'issue 123'",
        ),
        (
            "One change merged: `Big change _`_.",
            "Reference not found for 'big change <",
        ),
        (
            "Improved the `home page `_.

            .. _pull 91: https://github.com/joe/project/91
            """,
            "[WARNING] Reference not found for 'pull91'",
        ),
    ],
)
def test_bad_convert_to_markdown(rst_text, msg):
    with pytest.raises(ScrivException, match=re.escape(msg)):
        converted = RstTools().convert_to_markdown(
            textwrap.dedent(rst_text), fail_if_warn=True
        )
        # if we don't get the exception, we can debug the test:
        print(converted)
scriv-1.7.0/tests/test_ghrel.py000066400000000000000000000227031500123625400165230ustar00rootroot00000000000000"""Tests of scriv/ghrel.py."""

import json
import logging
from typing import Any
from unittest.mock import call

import pytest

from .helpers import check_logs

CHANGELOG1 = """\
Some text before

v1.2.3 -- 2022-04-21
--------------------

A good release

Some fixes
----------

No version number in this section.

v1.0 -- 2020-02-20
------------------

Nothing to say.

v0.9a7 -- 2017-06-16
--------------------

A beginning

v0.1 -- 2010-01-01
------------------

Didn't bother to tag this one.

v0.0.1 -- 2001-01-01
--------------------

Very first.

"""

RELEASES1 = {
    "v1.0": {
        "url": "https://api.github.com/repos/joe/project/releases/120",
        "body": "Nothing to say.\n",
    },
    "v0.9a7": {
        "url": "https://api.github.com/repos/joe/project/releases/123",
        "body": "original body",
    },
    "v0.0.1": {
        "url": "https://api.github.com/repos/joe/project/releases/123",
        "body": "original body",
    },
}

V123_REL = {
    "body": "A good release\n",
    "name": "v1.2.3",
    "tag_name": "v1.2.3",
    "draft": False,
    "prerelease": False,
}

V097_REL = {
    "body": "A beginning\n",
    "name": "v0.9a7",
    "tag_name": "v0.9a7",
    "draft": False,
    "prerelease": True,
}

V001_REL = {
    "body": "Very first.\n",
    "name": "v0.0.1",
    "tag_name": "v0.0.1",
    "draft": False,
    "prerelease": False,
}


@pytest.fixture()
def scenario1(temp_dir, fake_git, mocker):
    """A common scenario for the tests."""
    fake_git.add_remote("origin", "git@github.com:joe/project.git")
    fake_git.add_tags(["v1.2.3", "v1.0", "v0.9a7", "v0.0.1"])
    (temp_dir / "CHANGELOG.rst").write_text(CHANGELOG1)
    mock_get_releases = mocker.patch("scriv.ghrel.get_releases")
    mock_get_releases.return_value = RELEASES1


@pytest.fixture()
def mock_create_release(mocker):
    """Create a mock create_release that checks arguments."""

    def _create_release(repo: str, release_data: dict[str, Any]) -> None:
        assert repo
        assert release_data["name"]
        assert json.dumps(release_data)[0] == "{"

    return mocker.patch(
        "scriv.ghrel.create_release", side_effect=_create_release
    )


@pytest.fixture()
def mock_update_release(mocker):
    """Create a mock update_release that checks arguments."""

    def _update_release(
        release: dict[str, Any], release_data: dict[str, Any]
    ) -> None:
        assert release_data["name"]
        assert release["url"]
        assert json.dumps(release_data)[0] == "{"

    return mocker.patch(
        "scriv.ghrel.update_release", side_effect=_update_release
    )


def test_default(
    cli_invoke, scenario1, mock_create_release, mock_update_release, caplog
):
    cli_invoke(["github-release"])

    assert mock_create_release.mock_calls == [call("joe/project", V123_REL)]
    assert mock_update_release.mock_calls == []
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
    ]


def test_dash_all(
    cli_invoke, scenario1, mock_create_release, mock_update_release, caplog
):
    cli_invoke(["github-release", "--all"])

    assert mock_create_release.mock_calls == [call("joe/project", V123_REL)]
    assert mock_update_release.mock_calls == [
        call(RELEASES1["v0.9a7"], V097_REL),
        call(RELEASES1["v0.0.1"], V001_REL),
    ]
    assert caplog.record_tuples == [
        (
            "scriv.changelog",
            logging.INFO,
            "Reading changelog CHANGELOG.rst",
        ),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Entry 'Some fixes' has no version, skipping.",
        ),
        (
            "scriv.ghrel",
            logging.WARNING,
            "Version v0.1 has no tag. No release will be made.",
        ),
    ]


def test_explicit_repo(
    cli_invoke, scenario1, fake_git, mock_create_release, mock_update_release
):
    # Add another GitHub remote, now there are two.
    fake_git.add_remote("upstream", "git@github.com:psf/project.git")

    cli_invoke(["github-release", "--repo=xyzzy/plugh"])

    assert mock_create_release.mock_calls == [call("xyzzy/plugh", V123_REL)]
    assert mock_update_release.mock_calls == []


@pytest.mark.parametrize(
    "repo", ["xyzzy", "https://github.com/xyzzy/plugh.git"]
)
def test_bad_explicit_repo(cli_invoke, repo):
    result = cli_invoke(["github-release", f"--repo={repo}"], expect_ok=False)
    assert result.exit_code == 1
    assert str(result.exception) == f"Repo must be owner/reponame: {repo!r}"


def test_check_links(cli_invoke, scenario1, mocker):
    mock_check_links = mocker.patch("scriv.ghrel.check_markdown_links")
    cli_invoke(["github-release", "--all", "--dry-run", "--check-links"])
    assert mock_check_links.mock_calls == [
        call("A good release\n"),
        call("Nothing to say.\n"),
        call("A beginning\n"),
        call("Very first.\n"),
    ]


@pytest.fixture()
def no_actions(mock_create_release, mock_update_release, responses):
    """Check that nothing really happened."""

    yield

    assert mock_create_release.mock_calls == []
    assert mock_update_release.mock_calls == []
    assert len(responses.calls) == 0


def test_default_dry_run(cli_invoke, scenario1, no_actions, caplog):
    cli_invoke(["github-release", "--dry-run"])
    check_logs(
        caplog,
        [
            (
                "scriv.changelog",
                logging.INFO,
                "Reading changelog CHANGELOG.rst",
            ),
            ("scriv.ghrel", logging.INFO, "Would create release v1.2.3"),
        ],
    )


def test_dash_all_dry_run(cli_invoke, scenario1, no_actions, caplog):
    cli_invoke(["github-release", "--all", "--dry-run"])
    check_logs(
        caplog,
        [
            (
                "scriv.changelog",
                logging.INFO,
                "Reading changelog CHANGELOG.rst",
            ),
            ("scriv.ghrel", logging.INFO, "Would create release v1.2.3"),
            (
                "scriv.ghrel",
                logging.WARNING,
                "Entry 'Some fixes' has no version, skipping.",
            ),
            ("scriv.ghrel", logging.INFO, "Would update release v0.9a7"),
            (
                "scriv.ghrel",
                logging.WARNING,
                "Version v0.1 has no tag. No release will be made.",
            ),
            ("scriv.ghrel", 20, "Would update release v0.0.1"),
        ],
    )


def test_dash_all_dry_run_debug(cli_invoke, scenario1, no_actions, caplog):
    cli_invoke(["github-release", "--all", "--dry-run", "--verbosity=debug"])
    check_logs(
        caplog,
        [
            (
                "scriv.changelog",
                logging.INFO,
                "Reading changelog CHANGELOG.rst",
            ),
            (
                "scriv.ghrel",
                logging.DEBUG,
                "Creating release, data = {'body': 'A good release\\n', 'name': 'v1.2.3', "
                + "'tag_name': 'v1.2.3', 'draft': False, 'prerelease': False}",
            ),
            ("scriv.ghrel", logging.INFO, "Would create release v1.2.3"),
            ("scriv.ghrel", logging.DEBUG, "Body:\nA good release\n"),
            (
                "scriv.ghrel",
                logging.WARNING,
                "Entry 'Some fixes' has no version, skipping.",
            ),
            (
                "scriv.ghrel",
                logging.DEBUG,
                "Updating release v0.9a7, data = {'body': 'A beginning\\n', 'name': "
                + "'v0.9a7', 'tag_name': 'v0.9a7', 'draft': False, 'prerelease': True}",
            ),
            ("scriv.ghrel", logging.INFO, "Would update release v0.9a7"),
            ("scriv.ghrel", logging.DEBUG, "Old body:\noriginal body"),
            ("scriv.ghrel", logging.DEBUG, "New body:\nA beginning\n"),
            (
                "scriv.ghrel",
                logging.WARNING,
                "Version v0.1 has no tag. No release will be made.",
            ),
            (
                "scriv.ghrel",
                logging.DEBUG,
                "Updating release v0.0.1, data = {'body': 'Very first.\\n', 'name': "
                + "'v0.0.1', 'tag_name': 'v0.0.1', 'draft': False, 'prerelease': False}",
            ),
            ("scriv.ghrel", logging.INFO, "Would update release v0.0.1"),
            ("scriv.ghrel", logging.DEBUG, "Old body:\noriginal body"),
            ("scriv.ghrel", logging.DEBUG, "New body:\nVery first.\n"),
        ],
    )


def test_no_github_repo(cli_invoke, scenario1, fake_git):
    fake_git.remove_remote("origin")
    result = cli_invoke(["github-release"], expect_ok=False)
    assert result.exit_code == 1
    assert result.output == "Couldn't find a GitHub repo\n"


def test_no_clear_github_repo(cli_invoke, scenario1, fake_git):
    # Add another GitHub remote, now there are two.
    fake_git.add_remote("upstream", "git@github.com:psf/project.git")
    result = cli_invoke(["github-release"], expect_ok=False)
    assert result.exit_code == 1
    assert result.output == (
        "More than one GitHub repo found: joe/project, psf/project\n"
    )


def test_with_template(cli_invoke, temp_dir, scenario1, mock_create_release):
    (temp_dir / "setup.cfg").write_text(
        """
        [scriv]
        ghrel_template = |{{title}}|{{body}}|{{config.format}}|{{version}}|
        """
    )
    cli_invoke(["github-release"])

    expected = dict(V123_REL)
    expected["body"] = "|v1.2.3 -- 2022-04-21|A good release\n|rst|v1.2.3|"

    assert mock_create_release.mock_calls == [call("joe/project", expected)]
scriv-1.7.0/tests/test_github.py000066400000000000000000000110771500123625400167060ustar00rootroot00000000000000"""Tests of scriv/github.py"""

import json
import logging

import pytest
import requests

from scriv.github import (
    create_release,
    get_releases,
    github_paginated,
    update_release,
)


def test_one_page(responses):
    url = "https://api.github.com/repos/user/small_project/tags"
    data = [{"tag": word} for word in ["one", "two", "three", "four"]]
    responses.get(url, json=data)
    res = list(github_paginated(url))
    assert res == data


def test_three_pages(responses):
    # Three pages, referring to each other in the "link" header.
    url = "https://api.github.com/repos/user/large_project/tags"
    next_url = "https://api.github.com/repositories/138421996/tags"
    next_urls = [f"{next_url}?page={num}" for num in range(1, 4)]
    data = [
        [{"tag": f"{word}{num}"} for word in ["one", "two", "three", "four"]]
        for num in range(3)
    ]
    responses.get(
        url,
        json=data[0],
        headers={
            "link": f'<{next_urls[1]}>; rel="next", '
            + f'<{next_urls[2]}>; rel="last", ',
        },
    )
    responses.get(
        next_urls[1],
        json=data[1],
        headers={
            "link": f'<{next_urls[0]}>; rel="prev", '
            + f'<{next_urls[2]}>; rel="next", '
            + f'<{next_urls[2]}>; rel="last", '
            + f'<{next_urls[0]}>; rel="first"',
        },
    )
    responses.get(
        next_urls[2],
        json=data[2],
        headers={
            "link": f'<{next_urls[1]}>; rel="prev", '
            + f'<{next_urls[0]}>; rel="first"',
        },
    )
    res = list(github_paginated(url))
    assert res == data[0] + data[1] + data[2]


def test_bad_page(responses):
    url = "https://api.github.com/repos/user/small_project/secretstuff"
    responses.get(url, json=[], status=403)
    with pytest.raises(requests.HTTPError, match="403 Client Error"):
        list(github_paginated(url))


def test_get_releases(responses):
    url = "https://api.github.com/repos/user/small/releases"
    responses.get(
        url,
        json=[
            {"tag_name": "a", "name": "a", "prerelease": False},
            {"tag_name": "b", "name": "b", "prerelease": True},
        ],
    )
    releases = get_releases("user/small")
    assert releases == {
        "a": {"tag_name": "a", "name": "a", "prerelease": False},
        "b": {"tag_name": "b", "name": "b", "prerelease": True},
    }


RELEASE_DATA = {
    "name": "v3.14",
    "tag_name": "v3.14",
    "draft": False,
    "prerelease": False,
    "body": "this is a great release",
}


def test_create_release(responses, caplog):
    responses.post("https://api.github.com/repos/someone/something/releases")
    create_release("someone/something", RELEASE_DATA)
    assert json.loads(responses.calls[0].request.body) == RELEASE_DATA
    assert caplog.record_tuples == [
        ("scriv.github", logging.INFO, "Creating release v3.14")
    ]


def test_create_release_fails(responses):
    responses.post(
        "https://api.github.com/repos/someone/something/releases",
        status=500,
    )
    with pytest.raises(requests.HTTPError, match="500 Server Error"):
        create_release("someone/something", RELEASE_DATA)


def test_update_release(responses, caplog):
    url = "https://api.github.com/repos/someone/something/releases/60006815"
    responses.patch(url)
    release = {"url": url}
    update_release(release, RELEASE_DATA)
    assert json.loads(responses.calls[0].request.body) == RELEASE_DATA
    assert caplog.record_tuples == [
        ("scriv.github", logging.INFO, "Updating release v3.14")
    ]


def test_update_release_fails(responses):
    url = "https://api.github.com/repos/someone/something/releases/60006815"
    responses.patch(url, status=500)
    release = {"url": url}
    with pytest.raises(requests.HTTPError, match="500 Server Error"):
        update_release(release, RELEASE_DATA)


def test_authentication(responses, monkeypatch):
    # Neuter any .netrc file lying around.
    monkeypatch.setenv("NETRC", "no-such-file")

    url = "https://api.github.com/repos/user/project/something"

    responses.get(
        url,
        json=["logged in"],
        match=[
            responses.matchers.header_matcher(
                {"Authorization": "Bearer jabberwocky"}
            )
        ],
    )
    responses.get(
        url,
        json=["anonymous"],
        match=[responses.matchers.header_matcher({})],
    )

    res = list(github_paginated(url))
    assert res == ["anonymous"]

    with monkeypatch.context() as m:
        m.setenv("GITHUB_TOKEN", "jabberwocky")
        res = list(github_paginated(url))
        assert res == ["logged in"]
scriv-1.7.0/tests/test_gitinfo.py000066400000000000000000000051101500123625400170520ustar00rootroot00000000000000"""Tests of gitinfo.py"""

import re

from scriv.gitinfo import current_branch_name, get_github_repos, user_nick


def test_user_nick_from_scriv_user_nick(fake_git):
    fake_git.set_config("scriv.user-nick", "joedev")
    assert user_nick() == "joedev"


def test_user_nick_from_github(fake_git):
    fake_git.set_config("github.user", "joedev")
    assert user_nick() == "joedev"


def test_user_nick_from_git(fake_git):
    fake_git.set_config("user.email", "joesomeone@somewhere.org")
    assert user_nick() == "joesomeone"


def test_user_nick_from_env(fake_git, monkeypatch):
    monkeypatch.setenv("USER", "joseph")
    assert user_nick() == "joseph"


def test_user_nick_from_nowhere(fake_git, monkeypatch):
    # With no git information, and no USER env var,
    # we just call the user "somebody"
    monkeypatch.delenv("USER", raising=False)
    assert user_nick() == "somebody"


def test_current_branch_name(fake_git):
    fake_git.set_branch("joedev/feature-123")
    assert current_branch_name() == "joedev/feature-123"


def test_get_github_repos_no_remotes(fake_git):
    assert get_github_repos() == set()


def test_get_github_repos_one_github_remote(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_one_github_remote_no_extension(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_two_github_remotes(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@github.com:psf/myproject.git")
    assert get_github_repos() == {"joe/myproject", "psf/myproject"}


def test_get_github_repos_one_github_plus_others(fake_git):
    fake_git.add_remote("mygithub", "git@github.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@gitlab.com:psf/myproject.git")
    assert get_github_repos() == {"joe/myproject"}


def test_get_github_repos_no_github_remotes(fake_git):
    fake_git.add_remote("mygitlab", "git@gitlab.com:joe/myproject.git")
    fake_git.add_remote("upstream", "git@gitlab.com:psf/myproject.git")
    assert get_github_repos() == set()


def test_real_get_github_repos():
    # Since we don't know the name of this repo (forks could be anything),
    # we can't be sure what we get, except it should be word/word, and not end
    # with .git
    repos = get_github_repos()
    assert len(repos) >= 1
    repo = repos.pop()
    assert re.fullmatch(r"[\w-]+/[\w-]+", repo)
    assert not repo.endswith(".git")
scriv-1.7.0/tests/test_linkcheck.py000066400000000000000000000037211500123625400173540ustar00rootroot00000000000000"""Tests of scriv/linkcheck.py"""

import logging
import textwrap

import pytest

from scriv.linkcheck import check_markdown_links, find_links


@pytest.mark.parametrize(
    "markdown_text, links",
    [
        ("Hello", []),
        (
            """\
            [one](https://two.com/hello) and
            [two](https://one.com/xyzzy).
         """,
            ["https://one.com/xyzzy", "https://two.com/hello"],
        ),
        (
            """\
            This is [an example](http://example1.com/ "Title") inline link.
            This is [an example] [id] reference-style link.

            [id]: http://example2.com/  "Optional Title Here"
         """,
            ["http://example1.com/", "http://example2.com/"],
        ),
    ],
)
def test_find_links(markdown_text, links):
    found_links = sorted(find_links(textwrap.dedent(markdown_text)))
    assert links == found_links


def test_check_markdown_link(caplog, responses):
    caplog.set_level(logging.DEBUG, logger="scriv.linkcheck")
    responses.head("https://nedbat.com")
    check_markdown_links("""[hey](https://nedbat.com)!""")
    assert caplog.record_tuples == [
        (
            "scriv.linkcheck",
            logging.DEBUG,
            "OK link: 'https://nedbat.com'",
        )
    ]


def test_check_404_markdown_link(caplog, responses):
    responses.head("https://nedbat.com", status=404)
    check_markdown_links("""[hey](https://nedbat.com)!""")
    assert caplog.record_tuples == [
        (
            "scriv.linkcheck",
            logging.WARNING,
            "Failed check for 'https://nedbat.com': status code 404",
        )
    ]


def test_check_failing_markdown_link(caplog, responses):
    responses.head("https://nedbat.com", body=Exception("Buh?"))
    check_markdown_links("""[hey](https://nedbat.com)!""")
    assert caplog.record_tuples == [
        (
            "scriv.linkcheck",
            logging.WARNING,
            "Failed check for 'https://nedbat.com': Buh?",
        )
    ]
scriv-1.7.0/tests/test_literals.py000066400000000000000000000125501500123625400172400ustar00rootroot00000000000000"""Tests of literals.py"""

import os
import sys

import pytest

import scriv.literals
from scriv.exceptions import ScrivException
from scriv.literals import find_literal
from scriv.optional import tomllib, yaml


def test_no_extras_craziness():
    # Check that if we're testing no-extras we didn't get the modules, and if we
    # aren't, then we did get the modules.
    if os.getenv("SCRIV_TEST_NO_EXTRAS", ""):
        if sys.version_info < (3, 11):
            assert tomllib is None
        assert yaml is None
    else:
        assert tomllib is not None
        assert yaml is not None


PYTHON_CODE = """\
# A string we should get.
version = "1.2.3"

typed_version: Final[str] = "2.3.4"

thing1.attr = "3.4.5"
thing2.attr: str = "4.5.6"

# Numbers don't count.
how_many = 123

# Complex names don't count.
a_thing[0] = 123

# Non-constant values don't count.
a_thing_2 = func(1)

# Non-strings don't count.
version = compute_version(1)

if 1:
    # It's OK if they are inside other structures.
    also = "xyzzy"
    but = '''hello there'''

def foo():
    # Even in a function is OK, but why would you do that?
    somewhere_else = "this would be an odd place to get the string"
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
        ("typed_version", "2.3.4"),
        ("also", "xyzzy"),
        ("but", "hello there"),
        ("somewhere_else", "this would be an odd place to get the string"),
        ("a_thing_2", None),
        ("how_many", None),
    ],
)
def test_find_python_literal(name, value, temp_dir):
    with open("foo.py", "w", encoding="utf-8") as f:
        f.write(PYTHON_CODE)
    assert find_literal("foo.py", name) == value


def test_unknown_file_type(temp_dir):
    with open("what.xyz", "w", encoding="utf-8") as f:
        f.write("Hello there!")
    expected = "Can't read literals from files like 'what.xyz'"
    with pytest.raises(ScrivException, match=expected):
        find_literal("what.xyz", "hi")


TOML_LITERAL = """
version = "1"

[tool.poetry]
version = "2"

[metadata]
version = "3"
objects = { version = "4", other = "ignore" }

[bogus]
# Non-strings don't count.
number = 123
boolean = true
lists = [1, 2, 3]
bad_type = nan

# Sections don't count.
[bogus.section]

"""


@pytest.mark.skipif(tomllib is None, reason="No TOML support installed")
@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1"),
        ("tool.poetry.version", "2"),
        ("tool.poetry.version.too.deep", None),
        ("metadata.version", "3"),
        ("metadata.objects.version", "4"),
        ("bogus", None),
        ("bogus.number", None),
        ("bogus.boolean", None),
        ("bogus.lists", None),
        ("bogus.bad_type", None),
        ("bogus.section", None),
        ("bogus.section.too.deep", None),
    ],
)
def test_find_toml_literal(name, value, temp_dir):
    with open("foo.toml", "w", encoding="utf-8") as f:
        f.write(TOML_LITERAL)
    assert find_literal("foo.toml", name) == value


def test_find_toml_literal_fail_if_unavailable(monkeypatch):
    monkeypatch.setattr(scriv.literals, "tomllib", None)
    with pytest.raises(
        ScrivException, match="Can't read .+ without TOML support"
    ):
        find_literal("foo.toml", "fail")


YAML_LITERAL = """\
---
version: 1.2.3

myVersion:
  MAJOR: 2
  MINOR: 3
  PATCH: 5

myproduct:
  version: [mayor=5, minor=6, patch=7]
  versionString: "8.9.22"
...
"""


@pytest.mark.skipif(yaml is None, reason="No YAML support installed")
@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
        ("myproduct.versionString", "8.9.22"),
        ("myproduct.version", None),
        ("myVersion", None),
    ],
)
def test_find_yaml_literal(name, value, temp_dir):
    with open("foo.yml", "w", encoding="utf-8") as f:
        f.write(YAML_LITERAL)
    assert find_literal("foo.yml", name) == value


def test_find_yaml_literal_fail_if_unavailable(monkeypatch):
    monkeypatch.setattr(scriv.literals, "yaml", None)
    with pytest.raises(
        ScrivException, match="Can't read .+ without YAML support"
    ):
        find_literal("foo.yml", "fail")


CFG_LITERAL = """\

[metadata]
name = myproduct
version = 1.2.3
url = https://github.com/nedbat/scriv
description = A nice description
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT

[options]
zip_safe = false
include_package_data = true

[bdist_wheel]
universal = true

[coverage:report]
show_missing = true

[flake8]
max-line-length = 99
doctests = True
exclude =  .git, .eggs, __pycache__, tests/, docs/, build/, dist/
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("metadata.version", "1.2.3"),
        ("options.zip_safe", "false"),
        ("coverage:report", None),  # find_literal only supports string values
        ("metadata.myVersion", None),
        ("unexisting", None),
    ],
)
def test_find_cfg_literal(name, value, temp_dir):
    with open("foo.cfg", "w", encoding="utf-8") as f:
        f.write(CFG_LITERAL)
    assert find_literal("foo.cfg", name) == value


CABAL_LITERAL = """\
cabal-version:      3.0
name:               pkg
version:            1.2.3
"""


@pytest.mark.parametrize(
    "name, value",
    [
        ("version", "1.2.3"),
    ],
)
def test_find_cabal_literal(name, value, temp_dir):
    with open("foo.cabal", "w", encoding="utf-8") as f:
        f.write(CABAL_LITERAL)
    assert find_literal("foo.cabal", name) == value
scriv-1.7.0/tests/test_print.py000066400000000000000000000052261500123625400165570ustar00rootroot00000000000000"""Test print logic."""

import freezegun
import pytest

CHANGELOG_HEADER = """\

1.2 - 2020-02-25
================
"""


FRAG = """\
Fixed
-----

- Launching missiles no longer targets ourselves.
"""


@pytest.mark.parametrize("newline", ("\r\n", "\n"))
def test_print_fragment(newline, cli_invoke, changelog_d, temp_dir, capsys):
    fragment = FRAG.replace("\n", newline).encode("utf-8")
    (changelog_d / "20170616_nedbat.rst").write_bytes(fragment)

    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["print"])

    std = capsys.readouterr()
    assert std.out == FRAG


@pytest.mark.parametrize("newline", ("\r\n", "\n"))
def test_print_fragment_output(
    newline, cli_invoke, changelog_d, temp_dir, capsys
):
    fragment = FRAG.replace("\n", newline).encode("utf-8")
    (changelog_d / "20170616_nedbat.rst").write_bytes(fragment)
    output_file = temp_dir / "output.txt"

    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["print", "--output", output_file])

    std = capsys.readouterr()
    assert std.out == ""
    assert output_file.read_text().strip() == FRAG.strip()


@pytest.mark.parametrize("newline", ("\r\n", "\n"))
def test_print_changelog(newline, cli_invoke, changelog_d, temp_dir, capsys):
    changelog = (CHANGELOG_HEADER + FRAG).replace("\n", newline).encode("utf-8")
    (temp_dir / "CHANGELOG.rst").write_bytes(changelog)

    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["print", "--version", "1.2"])

    std = capsys.readouterr()
    assert std.out == FRAG


@pytest.mark.parametrize("newline", ("\r\n", "\n"))
def test_print_changelog_output(
    newline, cli_invoke, changelog_d, temp_dir, capsys
):
    changelog = (CHANGELOG_HEADER + FRAG).replace("\n", newline).encode("utf-8")
    (temp_dir / "CHANGELOG.rst").write_bytes(changelog)
    output_file = temp_dir / "output.txt"

    with freezegun.freeze_time("2020-02-25T15:18:19"):
        cli_invoke(["print", "--version", "1.2", "--output", output_file])

    std = capsys.readouterr()
    assert std.out == ""
    assert output_file.read_bytes().decode() == FRAG.strip().replace(
        "\n", newline
    )


def test_print_no_fragments(cli_invoke):
    result = cli_invoke(["print"], expect_ok=False)

    assert result.exit_code == 2
    assert "No changelog fragments to collect" in result.stderr


def test_print_version_not_in_changelog(cli_invoke, changelog_d, temp_dir):
    (temp_dir / "CHANGELOG.rst").write_bytes(b"BOGUS\n=====\n\n1.0\n===")

    result = cli_invoke(["print", "--version", "123.456"], expect_ok=False)

    assert result.exit_code == 2
    assert "Unable to find version 123.456 in the changelog" in result.stderr
scriv-1.7.0/tests/test_process.py000066400000000000000000000005471500123625400171020ustar00rootroot00000000000000"""Tests of the process behavior of scriv."""

import sys

from scriv import __version__
from scriv.shell import run_command


def test_dashm():
    ok, output = run_command([sys.executable, "-m", "scriv", "--help"])
    print(output)
    assert ok
    assert "Usage: scriv [OPTIONS] COMMAND [ARGS]..." in output
    assert "Version " + __version__ in output
scriv-1.7.0/tests/test_util.py000066400000000000000000000036671500123625400164070ustar00rootroot00000000000000"""Tests of scriv/util.py"""

import pytest

from scriv.util import Version, partition_lines


@pytest.mark.parametrize(
    "text, ver",
    [
        ("v1.2.3 -- 2022-04-06", "v1.2.3"),
        ("Oops, fixed on 6/16/2021.", None),
        ("2022-Apr-06: 12.3-alpha0 finally", "12.3-alpha0"),
        ("2.7.19beta1, 2022-04-08", "2.7.19beta1"),
    ],
)
def test_version_from_text(text, ver):
    if ver is not None:
        ver = Version(ver)
    assert Version.from_text(text) == ver


@pytest.mark.parametrize(
    "version",
    [
        "v1.2.3",
        "17.4.1.3",
    ],
)
def test_is_not_prerelease_version(version):
    assert not Version(version).is_prerelease()


@pytest.mark.parametrize(
    "version",
    [
        "v1.2.3a1",
        "17.4.1.3-beta.2",
    ],
)
def test_is_prerelease_version(version):
    assert Version(version).is_prerelease()


VERSION_EQUALITIES = [
    ("v1.2.3a1", "v1.2.3a1", True),
    ("1.2.3a1", "v1.2.3a1", True),
    ("v1.2.3a1", "1.2.3a1", True),
    ("1.2.3a1", "1.2.3a1", True),
    ("1.2", "1.2.0", False),
    ("1.2.3", "1.2.3a1", False),
    ("1.2.3a1", "1.2.3b1", False),
    ("v1.2.3", "1.2.3a1", False),
]


@pytest.mark.parametrize("ver1, ver2, equal", VERSION_EQUALITIES)
def test_version_equality(ver1, ver2, equal):
    assert (Version(ver1) == Version(ver2)) is equal


@pytest.mark.parametrize("ver1, ver2, equal", VERSION_EQUALITIES)
def test_version_hashing(ver1, ver2, equal):
    assert len({Version(ver1), Version(ver2)}) == (1 if equal else 2)


@pytest.mark.parametrize(
    "text, result",
    [
        ("one\ntwo\nthree\n", ("one\ntwo\nthree\n", "", "")),
        ("oXe\ntwo\nthree\n", ("", "oXe\n", "two\nthree\n")),
        ("one\ntXo\nthree\n", ("one\n", "tXo\n", "three\n")),
        ("one\ntwo\ntXree\n", ("one\ntwo\n", "tXree\n", "")),
        ("one\ntXo\ntXree\n", ("one\n", "tXo\n", "tXree\n")),
    ],
)
def test_partition_lines(text, result):
    assert partition_lines(text, "X") == result
scriv-1.7.0/tox.ini000066400000000000000000000053641500123625400141660ustar00rootroot00000000000000[tox]
envlist =
    # Show OS dependencies
    deps,
    # Run on all our Pythons:
    py3{9,10,11,12,13},
    # Run with no extras on lowest and highest version:
    py3{9,13}-no_extras,
    # And the rest:
    pypy3, coverage, docs, quality
labels =
    ci-tests = deps,py3{9,10,11,12,13},py3{9,13}-no_extras,pypy3

[testenv]
package = wheel
wheel_build_env = .pkg
deps =
    -r{toxinidir}/requirements/test.txt
    no_extras: pip
extras =
    !no_extras: toml,yaml
allowlist_externals =
    make
    rm
passenv =
    COVERAGE_*
setenv =
    no_extras: SCRIV_TEST_NO_EXTRAS=1
commands =
    no_extras: python -m pip uninstall -q -y tomli
    coverage run -p -m pytest -Wd -c tox.ini {posargs}

[testenv:.pkg]
# Force wheels to be built with the latest pip, wheel, and setuptools.
set_env =
    VIRTUALENV_DOWNLOAD=1

[testenv:deps]
allowlist_externals =
    pandoc
commands =
    pandoc --version

[testenv:coverage]
depends = py39,py310,py311,py312,py313,pypy3
basepython = python3.12
commands =
    coverage combine -q
    coverage report -m --skip-covered
    coverage html
    coverage json
parallel_show_output = true

[testenv:docs]
setenv =
    PYTHONPATH = {toxinidir}
deps =
    -r{toxinidir}/requirements/doc.txt
commands =
    make -C docs clean html
    doc8 -q --ignore-path docs/include README.rst docs

[testenv:quality]
deps =
    -r{toxinidir}/requirements/quality.txt
commands =
    black --check --diff --line-length=80 src/scriv tests docs
    python -m cogapp -cP --check --verbosity=1 docs/*.rst
    mypy src/scriv tests
    pylint src/scriv tests docs
    pycodestyle src/scriv tests docs
    pydocstyle src/scriv tests docs
    isort --check-only --diff -p scriv tests src/scriv
    python -Im build
    twine check --strict dist/*

[testenv:upgrade]
commands =
    python -m pip install -U pip
    make upgrade

# Tools needed for running tests need to be configured in tox.ini instead of
# pyproject.toml so that we can run tests without tomli installed on Pythons
# before 3.11.
#
# The py39-no_extras tox environment uninstalls tomli so that we can ensure
# scriv works properly in that world without tomli.  Tools like pytest and
# coverage will find settings in pyproject.toml, but then fail on 3.9 because
# they can't import tomli to read the settings.

[pytest]
addopts = -rfe
norecursedirs = .* docs requirements

[coverage:run]
branch = True
source =
    scriv
    tests
omit =
    */__main__.py

[coverage:report]
precision = 2
exclude_also =
    def __repr__

[coverage:paths]
source =
    src
    */site-packages

others =
    .
    */scriv

# Pycodestyle doesn't read from pyproject.toml, so configure it here.

[pycodestyle]
exclude = .git,.tox
; E203 = whitespace before ':'
; E501 line too long
; W503 line break before binary operator
ignore = E203,E501,W503