diff options
| author | natsuoto <[email protected]> | 2026-07-16 14:40:39 +0530 |
|---|---|---|
| committer | natsuoto <[email protected]> | 2026-07-16 14:40:39 +0530 |
| commit | b7ae2f3885d5ff84237c9da8bc0c64fe1a509465 (patch) | |
| tree | 27d8e35406931aad0f26c89bb4679e7c38c0e24c | |
| parent | 9d308c262788252cac26f5821cade327e18da7aa (diff) | |
| download | edify-b7ae2f3885d5ff84237c9da8bc0c64fe1a509465.tar.xz edify-b7ae2f3885d5ff84237c9da8bc0c64fe1a509465.zip | |
feat(release): changes/ fragment system + generator, .public-surface snapshot + CI migration gate, deprecation-warning + anchor harnesses, Trusted Publishing, relicense CHANGELOG entry
| -rw-r--r-- | .github/workflows/github-actions.yml | 35 | ||||
| -rw-r--r-- | .github/workflows/python-publish.yml | 33 | ||||
| -rw-r--r-- | .public-surface | 693 | ||||
| -rw-r--r-- | CHANGELOG.rst | 1 | ||||
| -rw-r--r-- | changes/README.rst | 55 | ||||
| -rw-r--r-- | tests/discipline/anchors.test.py | 66 | ||||
| -rw-r--r-- | tests/discipline/deprecations.test.py | 72 | ||||
| -rw-r--r-- | tests/docs/snapshots.test.py | 8 | ||||
| -rw-r--r-- | tests/snapshots/docs/deprecation-policy/0020.regex (renamed from tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex) | 0 | ||||
| -rw-r--r-- | tests/snapshots/docs/upgrading/0.3-to-1.0/0046.regex (renamed from tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex) | 0 | ||||
| -rw-r--r-- | tests/snapshots/docs/upgrading/0.3-to-1.0/0071.regex | 0 | ||||
| -rw-r--r-- | tests/tools/changes.test.py | 116 | ||||
| -rw-r--r-- | tests/tools/surface.test.py | 89 | ||||
| -rw-r--r-- | tools/changes.py | 65 | ||||
| -rw-r--r-- | tools/surface.py | 99 |
15 files changed, 1314 insertions, 18 deletions
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index f9f49d6..0fcb6bf 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -21,6 +21,10 @@ jobs: python: '3.11' command: 'uv run pyright edify' os: 'ubuntu-latest' + - name: 'public-surface' + python: '3.11' + command: 'uv run python tools/surface.py --check' + os: 'ubuntu-latest' - name: 'docs' python: '3.11' command: 'uv run --group docs sphinx-build -b html docs dist/docs && uv run --group docs sphinx-build -b linkcheck docs dist/docs' @@ -59,6 +63,37 @@ jobs: - name: ${{ matrix.name }} run: ${{ matrix.command }} + surface-migration-gate: + name: 'public-surface migration gate' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + timeout-minutes: 10 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 0 + - name: require a changes/ fragment when the public surface moves + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + if echo "$PR_TITLE" | grep -q '\[no-migration\]'; then + echo "PR marked [no-migration]; skipping the fragment requirement." + exit 0 + fi + if git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- .public-surface; then + echo "Public surface unchanged; no fragment required." + exit 0 + fi + added_fragments=$(git diff --name-only --diff-filter=A "$BASE_SHA" "$HEAD_SHA" -- 'changes/*.rst' | grep -v 'changes/README.rst' || true) + if [ -z "$added_fragments" ]; then + echo "::error::.public-surface changed but no changes/ fragment was added. Add a fragment describing the migration, or mark the PR title with [no-migration]." + exit 1 + fi + echo "Public surface changed and a changes/ fragment accompanies it:" + echo "$added_fragments" + install-without-regex-extra: name: 'install: edify (no [regex] extra)' runs-on: ubuntu-latest diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index f47ac56..d832902 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,10 +1,7 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. +# Publishes the Python package to PyPI via Trusted Publishing (OIDC). +# No long-lived API token: PyPI verifies the GitHub Actions OIDC identity of +# this workflow. Configure the trusted publisher once in the PyPI project +# settings (owner: luciferreeves, repo: edify, workflow: python-publish.yml). name: Upload Python Package @@ -18,23 +15,23 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest - + environment: + name: pypi + url: https://pypi.org/p/edify + permissions: + id-token: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build + - name: Install build backend + run: python -m pip install --upgrade pip build - name: Build package run: python -m build - - name: Publish package + - name: Publish package to PyPI via Trusted Publishing uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.public-surface b/.public-surface new file mode 100644 index 0000000..6bbbe50 --- /dev/null +++ b/.public-surface @@ -0,0 +1,693 @@ +edify.ALPHANUMERIC(value: 'str') -> 'bool' +edify.ANY_CHAR(value: 'str') -> 'bool' +edify.CARRIAGE_RETURN(value: 'str') -> 'bool' +edify.DIGIT(value: 'str') -> 'bool' +edify.END(value: 'str') -> 'bool' +edify.EdifyError +edify.EdifySyntaxError +edify.LETTER(value: 'str') -> 'bool' +edify.LOWERCASE(value: 'str') -> 'bool' +edify.NEW_LINE(value: 'str') -> 'bool' +edify.NON_DIGIT(value: 'str') -> 'bool' +edify.NON_WHITESPACE(value: 'str') -> 'bool' +edify.NON_WORD(value: 'str') -> 'bool' +edify.NON_WORD_BOUNDARY(value: 'str') -> 'bool' +edify.NULL_BYTE(value: 'str') -> 'bool' +edify.Pattern() -> 'None' +edify.Regex(source: 'str', compiled: 're.Pattern[str]', elements: 'tuple[BaseElement, ...]' = (), engine: 'Engine' = 're') -> 'None' +edify.RegexBuilder() -> 'None' +edify.START(value: 'str') -> 'bool' +edify.TAB(value: 'str') -> 'bool' +edify.UPPERCASE(value: 'str') -> 'bool' +edify.WHITESPACE(value: 'str') -> 'bool' +edify.WORD(value: 'str') -> 'bool' +edify.WORD_BOUNDARY(value: 'str') -> 'bool' +edify.any_of(*operands: 'BuilderProtocol') -> 'Pattern' +edify.assert_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.assert_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.assert_not_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.assert_not_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.at_least(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.at_most(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.atoms.alnum(value: 'str') -> 'bool' +edify.atoms.ascii(value: 'str') -> 'bool' +edify.atoms.base32(value: 'str') -> 'bool' +edify.atoms.base58(value: 'str') -> 'bool' +edify.atoms.base64(value: 'str') -> 'bool' +edify.atoms.base64url(value: 'str') -> 'bool' +edify.atoms.bic(value: 'str') -> 'bool' +edify.atoms.binnum(value: 'str') -> 'bool' +edify.atoms.boolean(value: 'str') -> 'bool' +edify.atoms.braces(value: 'str') -> 'bool' +edify.atoms.brackets(value: 'str') -> 'bool' +edify.atoms.cidr(value: 'str') -> 'bool' +edify.atoms.clock(value: 'str') -> 'bool' +edify.atoms.clock12(value: 'str') -> 'bool' +edify.atoms.creditcard(value: 'str') -> 'bool' +edify.atoms.currency(value: 'str') -> 'bool' +edify.atoms.day(value: 'str') -> 'bool' +edify.atoms.decimal(value: 'str') -> 'bool' +edify.atoms.duration(value: 'str') -> 'bool' +edify.atoms.email(value: 'str') -> 'bool' +edify.atoms.epoch(value: 'str') -> 'bool' +edify.atoms.extension(value: 'str') -> 'bool' +edify.atoms.filename(value: 'str') -> 'bool' +edify.atoms.filepath(value: 'str') -> 'bool' +edify.atoms.floatnum(value: 'str') -> 'bool' +edify.atoms.guid(value: 'str') -> 'bool' +edify.atoms.hexcolor(value: 'str') -> 'bool' +edify.atoms.hexnum(value: 'str') -> 'bool' +edify.atoms.hexstring(value: 'str') -> 'bool' +edify.atoms.hostname(value: 'str') -> 'bool' +edify.atoms.httpmethod(value: 'str') -> 'bool' +edify.atoms.httpstatus(value: 'str') -> 'bool' +edify.atoms.iban(value: 'str') -> 'bool' +edify.atoms.integer(value: 'str') -> 'bool' +edify.atoms.ipv4(value: 'str') -> 'bool' +edify.atoms.ipv6(value: 'str') -> 'bool' +edify.atoms.isodate(value: 'str') -> 'bool' +edify.atoms.isodatetime(value: 'str') -> 'bool' +edify.atoms.label(value: 'str') -> 'bool' +edify.atoms.letter(value: 'str') -> 'bool' +edify.atoms.line(value: 'str') -> 'bool' +edify.atoms.localpart(value: 'str') -> 'bool' +edify.atoms.lower(value: 'str') -> 'bool' +edify.atoms.mac(value: 'str') -> 'bool' +edify.atoms.md5(value: 'str') -> 'bool' +edify.atoms.mimetype(value: 'str') -> 'bool' +edify.atoms.money(value: 'str') -> 'bool' +edify.atoms.month(value: 'str') -> 'bool' +edify.atoms.natural(value: 'str') -> 'bool' +edify.atoms.nibble(value: 'str') -> 'bool' +edify.atoms.objectid(value: 'str') -> 'bool' +edify.atoms.octet(value: 'str') -> 'bool' +edify.atoms.octnum(value: 'str') -> 'bool' +edify.atoms.oid(value: 'str') -> 'bool' +edify.atoms.parens(value: 'str') -> 'bool' +edify.atoms.percent(value: 'str') -> 'bool' +edify.atoms.port(value: 'str') -> 'bool' +edify.atoms.printable(value: 'str') -> 'bool' +edify.atoms.protocol(value: 'str') -> 'bool' +edify.atoms.quoted(value: 'str') -> 'bool' +edify.atoms.ratio(value: 'str') -> 'bool' +edify.atoms.rgbcolor(value: 'str') -> 'bool' +edify.atoms.scheme(value: 'str') -> 'bool' +edify.atoms.scientific(value: 'str') -> 'bool' +edify.atoms.semver(value: 'str') -> 'bool' +edify.atoms.sha256(value: 'str') -> 'bool' +edify.atoms.signed(value: 'str') -> 'bool' +edify.atoms.slug(value: 'str') -> 'bool' +edify.atoms.space(value: 'str') -> 'bool' +edify.atoms.timezone(value: 'str') -> 'bool' +edify.atoms.tld(value: 'str') -> 'bool' +edify.atoms.truefalse(value: 'str') -> 'bool' +edify.atoms.ulid(value: 'str') -> 'bool' +edify.atoms.unsigned(value: 'str') -> 'bool' +edify.atoms.upper(value: 'str') -> 'bool' +edify.atoms.uri(value: 'str') -> 'bool' +edify.atoms.url(value: 'str') -> 'bool' +edify.atoms.username(value: 'str') -> 'bool' +edify.atoms.uuid(value: 'str') -> 'bool' +edify.atoms.weekday(value: 'str') -> 'bool' +edify.atoms.word(value: 'str') -> 'bool' +edify.atoms.year(value: 'str') -> 'bool' +edify.atoms.yesno(value: 'str') -> 'bool' +edify.back_reference(index: 'int') -> 'Pattern' +edify.between(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.between_lazy(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.builder.RegexBuilder() -> 'None' +edify.capture(operand: 'BuilderProtocol') -> 'Pattern' +edify.char(value: 'str') -> 'Pattern' +edify.chars(characters: 'str') -> 'Pattern' +edify.compile.escape_for_char_class(characters: 'str') -> 'str' +edify.compile.escape_special(value: 'str') -> 'str' +edify.compile.quantifier.quantifier_suffix(quantifier: 'QuantifierElement') -> 'str' +edify.compile.quantifier.render_quantifier(quantifier: 'QuantifierElement', render_element: 'ElementRenderer') -> 'str' +edify.compile.render_element(element: 'BaseElement') -> 'str' +edify.elements.BaseElement() -> None +edify.elements.Element +edify.elements.types.BaseElement() -> None +edify.elements.types.Element +edify.errors.EdifyError +edify.errors.EdifySyntaxError +edify.exactly(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.group(operand: 'BuilderProtocol') -> 'Pattern' +edify.introspect.explain_elements(elements: 'tuple[BaseElement, ...]') -> 'str' +edify.introspect.verbose_elements(elements: 'tuple[BaseElement, ...]') -> 'str' +edify.introspect.visualize_elements(elements: 'tuple[BaseElement, ...]', format: 'str' = 'ascii', engine: 'str' = 'ascii') -> 'str' +edify.library.abnf(value: 'str') -> 'bool' +edify.library.address(value: 'str') -> 'bool' +edify.library.address.cidr(value: 'str') -> 'bool' +edify.library.address.domain(value: 'str') -> 'bool' +edify.library.address.hostname(value: 'str') -> 'bool' +edify.library.address.ip(value: 'str') -> 'bool' +edify.library.address.ipv4(value: 'str') -> 'bool' +edify.library.address.ipv6(value: 'str') -> 'bool' +edify.library.address.path(value: 'str') -> 'bool' +edify.library.address.port(value: 'str') -> 'bool' +edify.library.address.ptr(value: 'str') -> 'bool' +edify.library.address.socket(value: 'str') -> 'bool' +edify.library.address.subdomain(value: 'str') -> 'bool' +edify.library.address.subnet(value: 'str') -> 'bool' +edify.library.address.tld(value: 'str') -> 'bool' +edify.library.address.uri(value: 'str') -> 'bool' +edify.library.address.url(value: 'str') -> 'bool' +edify.library.address.zip_code(value: 'str') -> 'bool' +edify.library.age(value: 'str') -> 'bool' +edify.library.aircraft(value: 'str') -> 'bool' +edify.library.alpha(value: 'str') -> 'bool' +edify.library.alphanumeric(value: 'str') -> 'bool' +edify.library.altitude(value: 'str') -> 'bool' +edify.library.antlr(value: 'str') -> 'bool' +edify.library.apache(value: 'str') -> 'bool' +edify.library.api.atom(value: 'str') -> 'bool' +edify.library.api.graphql(value: 'str') -> 'bool' +edify.library.api.hal(value: 'str') -> 'bool' +edify.library.api.jsonapi(value: 'str') -> 'bool' +edify.library.api.oauth(value: 'str') -> 'bool' +edify.library.api.openapi(value: 'str') -> 'bool' +edify.library.api.openid(value: 'str') -> 'bool' +edify.library.api.rss(value: 'str') -> 'bool' +edify.library.api.saml(value: 'str') -> 'bool' +edify.library.api.soap(value: 'str') -> 'bool' +edify.library.api.swagger(value: 'str') -> 'bool' +edify.library.api.webhook(value: 'str') -> 'bool' +edify.library.apikey(value: 'str') -> 'bool' +edify.library.arn(value: 'str') -> 'bool' +edify.library.arxiv(value: 'str') -> 'bool' +edify.library.ascii(value: 'str') -> 'bool' +edify.library.asin(value: 'str') -> 'bool' +edify.library.atom(value: 'str') -> 'bool' +edify.library.auth.apikey(value: 'str') -> 'bool' +edify.library.auth.bearer(value: 'str') -> 'bool' +edify.library.auth.challenge(value: 'str') -> 'bool' +edify.library.auth.csrf(value: 'str') -> 'bool' +edify.library.auth.hmac(value: 'str') -> 'bool' +edify.library.auth.jwt(value: 'str') -> 'bool' +edify.library.auth.mfa(value: 'str') -> 'bool' +edify.library.auth.mnemonic(value: 'str') -> 'bool' +edify.library.auth.otp(value: 'str') -> 'bool' +edify.library.auth.passkey(value: 'str') -> 'bool' +edify.library.auth.password(value: 'str', min_length: 'int | None' = None, max_length: 'int | None' = None, min_upper: 'int | None' = None, min_lower: 'int | None' = None, min_digit: 'int | None' = None, min_special: 'int | None' = None, special_chars: 'str | None' = None) -> 'bool' +edify.library.auth.pin(value: 'str') -> 'bool' +edify.library.auth.refresh(value: 'str') -> 'bool' +edify.library.auth.secret(value: 'str') -> 'bool' +edify.library.auth.session(value: 'str') -> 'bool' +edify.library.auth.signing(value: 'str') -> 'bool' +edify.library.auth.sso(value: 'str') -> 'bool' +edify.library.auth.token(value: 'str') -> 'bool' +edify.library.auth.webauthn(value: 'str') -> 'bool' +edify.library.avro(value: 'str') -> 'bool' +edify.library.barcode(value: 'str') -> 'bool' +edify.library.base(value: 'str') -> 'bool' +edify.library.bearer(value: 'str') -> 'bool' +edify.library.bearing(value: 'str') -> 'bool' +edify.library.bic(value: 'str') -> 'bool' +edify.library.blood(value: 'str') -> 'bool' +edify.library.bnf(value: 'str') -> 'bool' +edify.library.bump(value: 'str') -> 'bool' +edify.library.captcha(value: 'str') -> 'bool' +edify.library.card(value: 'str') -> 'bool' +edify.library.cargo(value: 'str') -> 'bool' +edify.library.certificate(value: 'str') -> 'bool' +edify.library.challenge(value: 'str') -> 'bool' +edify.library.charset(value: 'str') -> 'bool' +edify.library.checksum(value: 'str') -> 'bool' +edify.library.cidr(value: 'str') -> 'bool' +edify.library.codec(value: 'str') -> 'bool' +edify.library.color(value: 'str') -> 'bool' +edify.library.color.color(value: 'str') -> 'bool' +edify.library.color.filter(value: 'str') -> 'bool' +edify.library.color.gradient(value: 'str') -> 'bool' +edify.library.color.palette(value: 'str') -> 'bool' +edify.library.color.swatch(value: 'str') -> 'bool' +edify.library.component(value: 'str') -> 'bool' +edify.library.contact.address(value: 'str') -> 'bool' +edify.library.contact.email(value: 'str') -> 'bool' +edify.library.contact.email_rfc_5322(value: 'str') -> 'bool' +edify.library.contact.fax(value: 'str') -> 'bool' +edify.library.contact.handle(value: 'str') -> 'bool' +edify.library.contact.pager(value: 'str') -> 'bool' +edify.library.contact.phone(value: 'str') -> 'bool' +edify.library.contact.username(value: 'str') -> 'bool' +edify.library.cookie(value: 'str') -> 'bool' +edify.library.coordinate(value: 'str') -> 'bool' +edify.library.cron(value: 'str') -> 'bool' +edify.library.crypto(value: 'str') -> 'bool' +edify.library.csp(value: 'str') -> 'bool' +edify.library.csr(value: 'str') -> 'bool' +edify.library.csrf(value: 'str') -> 'bool' +edify.library.csv(value: 'str') -> 'bool' +edify.library.currency(value: 'str') -> 'bool' +edify.library.cusip(value: 'str') -> 'bool' +edify.library.data.avro(value: 'str') -> 'bool' +edify.library.data.csv(value: 'str') -> 'bool' +edify.library.data.hdf5(value: 'str') -> 'bool' +edify.library.data.html(value: 'str') -> 'bool' +edify.library.data.ini(value: 'str') -> 'bool' +edify.library.data.json(value: 'str') -> 'bool' +edify.library.data.msgpack(value: 'str') -> 'bool' +edify.library.data.orc(value: 'str') -> 'bool' +edify.library.data.parquet(value: 'str') -> 'bool' +edify.library.data.protobuf(value: 'str') -> 'bool' +edify.library.data.toml(value: 'str') -> 'bool' +edify.library.data.tsv(value: 'str') -> 'bool' +edify.library.data.xml(value: 'str') -> 'bool' +edify.library.data.yaml(value: 'str') -> 'bool' +edify.library.date(value: 'str') -> 'bool' +edify.library.datetime(value: 'str') -> 'bool' +edify.library.der(value: 'str') -> 'bool' +edify.library.dicom(value: 'str') -> 'bool' +edify.library.did(value: 'str') -> 'bool' +edify.library.digest(value: 'str') -> 'bool' +edify.library.docker(value: 'str') -> 'bool' +edify.library.document.docx(value: 'str') -> 'bool' +edify.library.document.epub(value: 'str') -> 'bool' +edify.library.document.mobi(value: 'str') -> 'bool' +edify.library.document.odt(value: 'str') -> 'bool' +edify.library.document.pdf(value: 'str') -> 'bool' +edify.library.document.pptx(value: 'str') -> 'bool' +edify.library.document.readme(value: 'str') -> 'bool' +edify.library.document.rtf(value: 'str') -> 'bool' +edify.library.document.svg(value: 'str') -> 'bool' +edify.library.document.tex(value: 'str') -> 'bool' +edify.library.document.xlsx(value: 'str') -> 'bool' +edify.library.docx(value: 'str') -> 'bool' +edify.library.doi(value: 'str') -> 'bool' +edify.library.domain(value: 'str') -> 'bool' +edify.library.dosage(value: 'str') -> 'bool' +edify.library.duration(value: 'str') -> 'bool' +edify.library.ebnf(value: 'str') -> 'bool' +edify.library.ein(value: 'str') -> 'bool' +edify.library.email(value: 'str') -> 'bool' +edify.library.email_rfc_5322(value: 'str') -> 'bool' +edify.library.emoji(value: 'str') -> 'bool' +edify.library.encoding(value: 'str') -> 'bool' +edify.library.epoch(value: 'str') -> 'bool' +edify.library.epub(value: 'str') -> 'bool' +edify.library.extension(value: 'str') -> 'bool' +edify.library.favicon(value: 'str') -> 'bool' +edify.library.fax(value: 'str') -> 'bool' +edify.library.filename(value: 'str') -> 'bool' +edify.library.filter(value: 'str') -> 'bool' +edify.library.financial.card(value: 'str') -> 'bool' +edify.library.financial.crypto(value: 'str') -> 'bool' +edify.library.financial.currency(value: 'str') -> 'bool' +edify.library.financial.routing(value: 'str') -> 'bool' +edify.library.financial.sortcode(value: 'str') -> 'bool' +edify.library.financial.vat(value: 'str') -> 'bool' +edify.library.financial.wallet(value: 'str') -> 'bool' +edify.library.flight(value: 'str') -> 'bool' +edify.library.fraction(value: 'str') -> 'bool' +edify.library.geo.altitude(value: 'str') -> 'bool' +edify.library.geo.bearing(value: 'str') -> 'bool' +edify.library.geo.coordinate(value: 'str') -> 'bool' +edify.library.geo.geohash(value: 'str') -> 'bool' +edify.library.geo.mgrs(value: 'str') -> 'bool' +edify.library.geo.place(value: 'str') -> 'bool' +edify.library.geo.plus(value: 'str') -> 'bool' +edify.library.geo.postal(value: 'str') -> 'bool' +edify.library.geohash(value: 'str') -> 'bool' +edify.library.git(value: 'str') -> 'bool' +edify.library.glob(value: 'str') -> 'bool' +edify.library.gradient(value: 'str') -> 'bool' +edify.library.grammar.abnf(value: 'str') -> 'bool' +edify.library.grammar.antlr(value: 'str') -> 'bool' +edify.library.grammar.bnf(value: 'str') -> 'bool' +edify.library.grammar.ebnf(value: 'str') -> 'bool' +edify.library.grammar.peg(value: 'str') -> 'bool' +edify.library.grammar.pest(value: 'str') -> 'bool' +edify.library.graphql(value: 'str') -> 'bool' +edify.library.gtin(value: 'str') -> 'bool' +edify.library.guid(value: 'str') -> 'bool' +edify.library.hal(value: 'str') -> 'bool' +edify.library.handle(value: 'str') -> 'bool' +edify.library.hash(value: 'str') -> 'bool' +edify.library.hdf5(value: 'str') -> 'bool' +edify.library.hmac(value: 'str') -> 'bool' +edify.library.hostname(value: 'str') -> 'bool' +edify.library.htaccess(value: 'str') -> 'bool' +edify.library.html(value: 'str') -> 'bool' +edify.library.humans(value: 'str') -> 'bool' +edify.library.iata(value: 'str') -> 'bool' +edify.library.iban(value: 'str') -> 'bool' +edify.library.icao(value: 'str') -> 'bool' +edify.library.iccid(value: 'str') -> 'bool' +edify.library.identifier.arn(value: 'str') -> 'bool' +edify.library.identifier.asin(value: 'str') -> 'bool' +edify.library.identifier.bic(value: 'str') -> 'bool' +edify.library.identifier.cusip(value: 'str') -> 'bool' +edify.library.identifier.did(value: 'str') -> 'bool' +edify.library.identifier.ein(value: 'str') -> 'bool' +edify.library.identifier.guid(value: 'str') -> 'bool' +edify.library.identifier.iata(value: 'str') -> 'bool' +edify.library.identifier.iban(value: 'str') -> 'bool' +edify.library.identifier.icao(value: 'str') -> 'bool' +edify.library.identifier.iccid(value: 'str') -> 'bool' +edify.library.identifier.imei(value: 'str') -> 'bool' +edify.library.identifier.imo(value: 'str') -> 'bool' +edify.library.identifier.isin(value: 'str') -> 'bool' +edify.library.identifier.itin(value: 'str') -> 'bool' +edify.library.identifier.lei(value: 'str') -> 'bool' +edify.library.identifier.mac(value: 'str') -> 'bool' +edify.library.identifier.meid(value: 'str') -> 'bool' +edify.library.identifier.mmsi(value: 'str') -> 'bool' +edify.library.identifier.orcid(value: 'str') -> 'bool' +edify.library.identifier.sedol(value: 'str') -> 'bool' +edify.library.identifier.sku(value: 'str') -> 'bool' +edify.library.identifier.ssn(value: 'str') -> 'bool' +edify.library.identifier.tin(value: 'str') -> 'bool' +edify.library.identifier.uuid(value: 'str') -> 'bool' +edify.library.identifier.vin(value: 'str') -> 'bool' +edify.library.image(value: 'str') -> 'bool' +edify.library.imei(value: 'str') -> 'bool' +edify.library.imo(value: 'str') -> 'bool' +edify.library.ini(value: 'str') -> 'bool' +edify.library.integer(value: 'str') -> 'bool' +edify.library.interval(value: 'str') -> 'bool' +edify.library.ip(value: 'str') -> 'bool' +edify.library.ipv4(value: 'str') -> 'bool' +edify.library.ipv6(value: 'str') -> 'bool' +edify.library.isbn(value: 'str') -> 'bool' +edify.library.isin(value: 'str') -> 'bool' +edify.library.iso_date(value: 'str') -> 'bool' +edify.library.issn(value: 'str') -> 'bool' +edify.library.itin(value: 'str') -> 'bool' +edify.library.json(value: 'str') -> 'bool' +edify.library.jsonapi(value: 'str') -> 'bool' +edify.library.jwt(value: 'str') -> 'bool' +edify.library.keyring(value: 'str') -> 'bool' +edify.library.lei(value: 'str') -> 'bool' +edify.library.locale(value: 'str') -> 'bool' +edify.library.mac(value: 'str') -> 'bool' +edify.library.makefile(value: 'str') -> 'bool' +edify.library.manifest(value: 'str') -> 'bool' +edify.library.media.charset(value: 'str') -> 'bool' +edify.library.media.codec(value: 'str') -> 'bool' +edify.library.media.encoding(value: 'str') -> 'bool' +edify.library.media.extension(value: 'str') -> 'bool' +edify.library.media.favicon(value: 'str') -> 'bool' +edify.library.media.filename(value: 'str') -> 'bool' +edify.library.media.glob(value: 'str') -> 'bool' +edify.library.media.locale(value: 'str') -> 'bool' +edify.library.media.mimetype(value: 'str') -> 'bool' +edify.library.media.regex(value: 'str') -> 'bool' +edify.library.media.shebang(value: 'str') -> 'bool' +edify.library.medical(value: 'str') -> 'bool' +edify.library.medical.blood(value: 'str') -> 'bool' +edify.library.medical.dicom(value: 'str') -> 'bool' +edify.library.medical.dosage(value: 'str') -> 'bool' +edify.library.medical.medical(value: 'str') -> 'bool' +edify.library.meid(value: 'str') -> 'bool' +edify.library.mfa(value: 'str') -> 'bool' +edify.library.mgrs(value: 'str') -> 'bool' +edify.library.mimetype(value: 'str') -> 'bool' +edify.library.mmsi(value: 'str') -> 'bool' +edify.library.mnemonic(value: 'str') -> 'bool' +edify.library.mobi(value: 'str') -> 'bool' +edify.library.mpn(value: 'str') -> 'bool' +edify.library.msgpack(value: 'str') -> 'bool' +edify.library.natural(value: 'str') -> 'bool' +edify.library.nginx(value: 'str') -> 'bool' +edify.library.nonce(value: 'str') -> 'bool' +edify.library.number(value: 'str') -> 'bool' +edify.library.numeric(value: 'str') -> 'bool' +edify.library.numeric.fraction(value: 'str') -> 'bool' +edify.library.numeric.hash(value: 'str') -> 'bool' +edify.library.numeric.integer(value: 'str') -> 'bool' +edify.library.numeric.natural(value: 'str') -> 'bool' +edify.library.numeric.number(value: 'str') -> 'bool' +edify.library.numeric.ordinal(value: 'str') -> 'bool' +edify.library.numeric.percentage(value: 'str') -> 'bool' +edify.library.numeric.ratio(value: 'str') -> 'bool' +edify.library.numeric.roman(value: 'str') -> 'bool' +edify.library.numeric.scientific(value: 'str') -> 'bool' +edify.library.oauth(value: 'str') -> 'bool' +edify.library.odt(value: 'str') -> 'bool' +edify.library.offset(value: 'str') -> 'bool' +edify.library.openapi(value: 'str') -> 'bool' +edify.library.openid(value: 'str') -> 'bool' +edify.library.orc(value: 'str') -> 'bool' +edify.library.orcid(value: 'str') -> 'bool' +edify.library.ordinal(value: 'str') -> 'bool' +edify.library.otp(value: 'str') -> 'bool' +edify.library.package(value: 'str') -> 'bool' +edify.library.pager(value: 'str') -> 'bool' +edify.library.palette(value: 'str') -> 'bool' +edify.library.parquet(value: 'str') -> 'bool' +edify.library.passkey(value: 'str') -> 'bool' +edify.library.password(value: 'str', min_length: 'int | None' = None, max_length: 'int | None' = None, min_upper: 'int | None' = None, min_lower: 'int | None' = None, min_digit: 'int | None' = None, min_special: 'int | None' = None, special_chars: 'str | None' = None) -> 'bool' +edify.library.path(value: 'str') -> 'bool' +edify.library.pdf(value: 'str') -> 'bool' +edify.library.peg(value: 'str') -> 'bool' +edify.library.pem(value: 'str') -> 'bool' +edify.library.percentage(value: 'str') -> 'bool' +edify.library.pest(value: 'str') -> 'bool' +edify.library.pgp(value: 'str') -> 'bool' +edify.library.phone(value: 'str') -> 'bool' +edify.library.pin(value: 'str') -> 'bool' +edify.library.place(value: 'str') -> 'bool' +edify.library.plate(value: 'str') -> 'bool' +edify.library.plus(value: 'str') -> 'bool' +edify.library.pmc(value: 'str') -> 'bool' +edify.library.pmid(value: 'str') -> 'bool' +edify.library.port(value: 'str') -> 'bool' +edify.library.postal(value: 'str') -> 'bool' +edify.library.pptx(value: 'str') -> 'bool' +edify.library.printable(value: 'str') -> 'bool' +edify.library.product.barcode(value: 'str') -> 'bool' +edify.library.product.gtin(value: 'str') -> 'bool' +edify.library.product.mpn(value: 'str') -> 'bool' +edify.library.protobuf(value: 'str') -> 'bool' +edify.library.ptr(value: 'str') -> 'bool' +edify.library.publishing.arxiv(value: 'str') -> 'bool' +edify.library.publishing.doi(value: 'str') -> 'bool' +edify.library.publishing.isbn(value: 'str') -> 'bool' +edify.library.publishing.issn(value: 'str') -> 'bool' +edify.library.publishing.pmc(value: 'str') -> 'bool' +edify.library.publishing.pmid(value: 'str') -> 'bool' +edify.library.ratio(value: 'str') -> 'bool' +edify.library.readme(value: 'str') -> 'bool' +edify.library.ref(value: 'str') -> 'bool' +edify.library.refresh(value: 'str') -> 'bool' +edify.library.regex(value: 'str') -> 'bool' +edify.library.robots(value: 'str') -> 'bool' +edify.library.roman(value: 'str') -> 'bool' +edify.library.routing(value: 'str') -> 'bool' +edify.library.rss(value: 'str') -> 'bool' +edify.library.rtf(value: 'str') -> 'bool' +edify.library.saml(value: 'str') -> 'bool' +edify.library.scientific(value: 'str') -> 'bool' +edify.library.script(value: 'str') -> 'bool' +edify.library.secret(value: 'str') -> 'bool' +edify.library.security.age(value: 'str') -> 'bool' +edify.library.security.certificate(value: 'str') -> 'bool' +edify.library.security.csr(value: 'str') -> 'bool' +edify.library.security.der(value: 'str') -> 'bool' +edify.library.security.keyring(value: 'str') -> 'bool' +edify.library.security.nonce(value: 'str') -> 'bool' +edify.library.security.pem(value: 'str') -> 'bool' +edify.library.security.pgp(value: 'str') -> 'bool' +edify.library.security.signature(value: 'str') -> 'bool' +edify.library.security.ssh(value: 'str') -> 'bool' +edify.library.security.x509(value: 'str') -> 'bool' +edify.library.sedol(value: 'str') -> 'bool' +edify.library.semver(value: 'str') -> 'bool' +edify.library.session(value: 'str') -> 'bool' +edify.library.shebang(value: 'str') -> 'bool' +edify.library.signature(value: 'str') -> 'bool' +edify.library.signing(value: 'str') -> 'bool' +edify.library.sitemap(value: 'str') -> 'bool' +edify.library.sku(value: 'str') -> 'bool' +edify.library.slug(value: 'str') -> 'bool' +edify.library.soap(value: 'str') -> 'bool' +edify.library.socket(value: 'str') -> 'bool' +edify.library.software.bump(value: 'str') -> 'bool' +edify.library.software.cargo(value: 'str') -> 'bool' +edify.library.software.checksum(value: 'str') -> 'bool' +edify.library.software.component(value: 'str') -> 'bool' +edify.library.software.digest(value: 'str') -> 'bool' +edify.library.software.docker(value: 'str') -> 'bool' +edify.library.software.git(value: 'str') -> 'bool' +edify.library.software.image(value: 'str') -> 'bool' +edify.library.software.makefile(value: 'str') -> 'bool' +edify.library.software.package(value: 'str') -> 'bool' +edify.library.software.ref(value: 'str') -> 'bool' +edify.library.software.semver(value: 'str') -> 'bool' +edify.library.software.version(value: 'str') -> 'bool' +edify.library.sortcode(value: 'str') -> 'bool' +edify.library.ssh(value: 'str') -> 'bool' +edify.library.ssn(value: 'str') -> 'bool' +edify.library.sso(value: 'str') -> 'bool' +edify.library.subdomain(value: 'str') -> 'bool' +edify.library.subnet(value: 'str') -> 'bool' +edify.library.svg(value: 'str') -> 'bool' +edify.library.swagger(value: 'str') -> 'bool' +edify.library.swatch(value: 'str') -> 'bool' +edify.library.temporal.cron(value: 'str') -> 'bool' +edify.library.temporal.date(value: 'str') -> 'bool' +edify.library.temporal.datetime(value: 'str') -> 'bool' +edify.library.temporal.duration(value: 'str') -> 'bool' +edify.library.temporal.epoch(value: 'str') -> 'bool' +edify.library.temporal.interval(value: 'str') -> 'bool' +edify.library.temporal.iso_date(value: 'str') -> 'bool' +edify.library.temporal.offset(value: 'str') -> 'bool' +edify.library.temporal.time(value: 'str') -> 'bool' +edify.library.temporal.timestamp(value: 'str') -> 'bool' +edify.library.temporal.timezone(value: 'str') -> 'bool' +edify.library.temporal.year(value: 'str') -> 'bool' +edify.library.tex(value: 'str') -> 'bool' +edify.library.text.alpha(value: 'str') -> 'bool' +edify.library.text.alphanumeric(value: 'str') -> 'bool' +edify.library.text.ascii(value: 'str') -> 'bool' +edify.library.text.base(value: 'str') -> 'bool' +edify.library.text.emoji(value: 'str') -> 'bool' +edify.library.text.numeric(value: 'str') -> 'bool' +edify.library.text.printable(value: 'str') -> 'bool' +edify.library.text.script(value: 'str') -> 'bool' +edify.library.text.slug(value: 'str') -> 'bool' +edify.library.text.unicode(value: 'str') -> 'bool' +edify.library.text.word(value: 'str') -> 'bool' +edify.library.time(value: 'str') -> 'bool' +edify.library.timestamp(value: 'str') -> 'bool' +edify.library.timezone(value: 'str') -> 'bool' +edify.library.tin(value: 'str') -> 'bool' +edify.library.tld(value: 'str') -> 'bool' +edify.library.token(value: 'str') -> 'bool' +edify.library.toml(value: 'str') -> 'bool' +edify.library.transport.aircraft(value: 'str') -> 'bool' +edify.library.transport.flight(value: 'str') -> 'bool' +edify.library.transport.plate(value: 'str') -> 'bool' +edify.library.transport.vehicle(value: 'str') -> 'bool' +edify.library.tsv(value: 'str') -> 'bool' +edify.library.unicode(value: 'str') -> 'bool' +edify.library.uri(value: 'str') -> 'bool' +edify.library.url(value: 'str') -> 'bool' +edify.library.useragent(value: 'str') -> 'bool' +edify.library.username(value: 'str') -> 'bool' +edify.library.uuid(value: 'str') -> 'bool' +edify.library.vat(value: 'str') -> 'bool' +edify.library.vehicle(value: 'str') -> 'bool' +edify.library.version(value: 'str') -> 'bool' +edify.library.vin(value: 'str') -> 'bool' +edify.library.wallet(value: 'str') -> 'bool' +edify.library.web.apache(value: 'str') -> 'bool' +edify.library.web.captcha(value: 'str') -> 'bool' +edify.library.web.cookie(value: 'str') -> 'bool' +edify.library.web.csp(value: 'str') -> 'bool' +edify.library.web.htaccess(value: 'str') -> 'bool' +edify.library.web.humans(value: 'str') -> 'bool' +edify.library.web.manifest(value: 'str') -> 'bool' +edify.library.web.nginx(value: 'str') -> 'bool' +edify.library.web.robots(value: 'str') -> 'bool' +edify.library.web.sitemap(value: 'str') -> 'bool' +edify.library.web.useragent(value: 'str') -> 'bool' +edify.library.webauthn(value: 'str') -> 'bool' +edify.library.webhook(value: 'str') -> 'bool' +edify.library.word(value: 'str') -> 'bool' +edify.library.x509(value: 'str') -> 'bool' +edify.library.xlsx(value: 'str') -> 'bool' +edify.library.xml(value: 'str') -> 'bool' +edify.library.yaml(value: 'str') -> 'bool' +edify.library.year(value: 'str') -> 'bool' +edify.library.zip_code(value: 'str') -> 'bool' +edify.named_back_reference(name: 'str') -> 'Pattern' +edify.named_capture(name: 'str', operand: 'BuilderProtocol') -> 'Pattern' +edify.nonchars(characters: 'str') -> 'Pattern' +edify.nonrange(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.nonstring(value: 'str') -> 'Pattern' +edify.one_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.one_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' +edify.optional(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.ALPHANUMERIC(value: 'str') -> 'bool' +edify.pattern.ANY_CHAR(value: 'str') -> 'bool' +edify.pattern.CARRIAGE_RETURN(value: 'str') -> 'bool' +edify.pattern.DIGIT(value: 'str') -> 'bool' +edify.pattern.END(value: 'str') -> 'bool' +edify.pattern.LETTER(value: 'str') -> 'bool' +edify.pattern.LOWERCASE(value: 'str') -> 'bool' +edify.pattern.NEW_LINE(value: 'str') -> 'bool' +edify.pattern.NON_DIGIT(value: 'str') -> 'bool' +edify.pattern.NON_WHITESPACE(value: 'str') -> 'bool' +edify.pattern.NON_WORD(value: 'str') -> 'bool' +edify.pattern.NON_WORD_BOUNDARY(value: 'str') -> 'bool' +edify.pattern.NULL_BYTE(value: 'str') -> 'bool' +edify.pattern.Pattern() -> 'None' +edify.pattern.START(value: 'str') -> 'bool' +edify.pattern.TAB(value: 'str') -> 'bool' +edify.pattern.UPPERCASE(value: 'str') -> 'bool' +edify.pattern.WHITESPACE(value: 'str') -> 'bool' +edify.pattern.WORD(value: 'str') -> 'bool' +edify.pattern.WORD_BOUNDARY(value: 'str') -> 'bool' +edify.pattern.any_of(*operands: 'BuilderProtocol') -> 'Pattern' +edify.pattern.assert_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.assert_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.assert_not_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.assert_not_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.at_least(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.at_most(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.back_reference(index: 'int') -> 'Pattern' +edify.pattern.between(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.between_lazy(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.capture(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.char(value: 'str') -> 'Pattern' +edify.pattern.chars(characters: 'str') -> 'Pattern' +edify.pattern.exactly(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.any_of(*operands: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.assert_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.assert_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.assert_not_ahead(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.assert_not_behind(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.at_least(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.at_most(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.back_reference(index: 'int') -> 'Pattern' +edify.pattern.factories.between(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.between_lazy(lower: 'int', upper: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.capture(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.char(value: 'str') -> 'Pattern' +edify.pattern.factories.chars(characters: 'str') -> 'Pattern' +edify.pattern.factories.exactly(count: 'int', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.group(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.named_back_reference(name: 'str') -> 'Pattern' +edify.pattern.factories.named_capture(name: 'str', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.nonchars(characters: 'str') -> 'Pattern' +edify.pattern.factories.nonrange(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.pattern.factories.nonstring(value: 'str') -> 'Pattern' +edify.pattern.factories.one_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.one_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.optional(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.range_of(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.pattern.factories.string(value: 'str') -> 'Pattern' +edify.pattern.factories.zero_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.factories.zero_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.group(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.named_back_reference(name: 'str') -> 'Pattern' +edify.pattern.named_capture(name: 'str', operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.nonchars(characters: 'str') -> 'Pattern' +edify.pattern.nonrange(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.pattern.nonstring(value: 'str') -> 'Pattern' +edify.pattern.one_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.one_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.optional(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.range_of(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.pattern.string(value: 'str') -> 'Pattern' +edify.pattern.zero_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.pattern.zero_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' +edify.range_of(start_character: 'str', end_character: 'str') -> 'Pattern' +edify.result.Match(wrapped_match: 're.Match[str]') -> 'None' +edify.result.NamedCaptures(wrapped_match: 're.Match[str]') -> 'None' +edify.result.Regex(source: 'str', compiled: 're.Pattern[str]', elements: 'tuple[BaseElement, ...]' = (), engine: 'Engine' = 're') -> 'None' +edify.serialize.JSONPrimitive +edify.serialize.JSONValue +edify.serialize.SCHEMA_VERSION +edify.serialize.dict_to_element(tree: 'dict[str, JSONValue]') -> 'BaseElement' +edify.serialize.dict_to_state(document: 'dict[str, JSONValue]') -> 'BuilderState' +edify.serialize.element_to_dict(element: 'BaseElement') -> 'dict[str, JSONValue]' +edify.serialize.state_to_dict(state: 'BuilderState') -> 'dict[str, JSONValue]' +edify.string(value: 'str') -> 'Pattern' +edify.testing.SnapshotMismatchError(snapshot_path: 'Path', actual: 'str', expected: 'str') -> 'None' +edify.testing.SnapshotMissingError(snapshot_path: 'Path') -> 'None' +edify.testing.assert_snapshot(actual: 'str', snapshot_path: 'Path') -> 'None' +edify.validate.is_valid_group_name(name: 'str') -> 'bool' +edify.zero_or_more(operand: 'BuilderProtocol') -> 'Pattern' +edify.zero_or_more_lazy(operand: 'BuilderProtocol') -> 'Pattern' diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9453464..ad6869c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ---------- +* **Changed:** Relicensed from Apache-2.0 to the `MIT License <https://github.com/luciferreeves/edify/blob/main/LICENSE>`_. No API change. See the :ref:`license-mit` note in the 0.3 → 1.0 upgrade guide for the patent-grant and pinned-tags details. * **Breaking:** Dropped support for Python 3.8. Edify now requires Python 3.9 or newer (:pr:`64`). Python 3.8 reached EOL on 2024-10-07; dependencies are now actively dropping it (e.g. ``virtualenv`` 21.5+ requires ``>=3.9``). * **Tooling and CI:** Dropped macOS and Windows runners from the CI matrix; Linux-only from here on. Edify is a pure-Python wheel with no platform-specific code, so the multi-OS jobs were buying ~zero signal and produced false negatives. Branch-protection required contexts went from 31 → 13 in lockstep (and now 12 with the Python 3.8 drop) (:pr:`60`). * **Dependencies:** Bumped the minimum ``virtualenv`` floor for the CI bootstrap to ``>=21.4.2`` (:pr:`56`, :pr:`58`). diff --git a/changes/README.rst b/changes/README.rst new file mode 100644 index 0000000..7a19e14 --- /dev/null +++ b/changes/README.rst @@ -0,0 +1,55 @@ +Change fragments +================ + +Every breaking change to Edify's public surface lands with a **change fragment** +in this directory. Fragments are the single source of truth for the CHANGELOG and +the upgrade guide: the release generator (``tools/changes.py``) concatenates them +into both outputs, then deletes them. + +There is no external changelog tooling — the format is a small stdlib-parseable +INI, and the generator is a single stdlib script. + +File format +----------- + +One file per breaking change, named ``<fragment-id>.rst`` where ``<fragment-id>`` +sorts into the order the fragment should appear in the release notes (for +example ``0010-char-class-escape.rst``). The file is an INI document with a single +``[change]`` section: + +.. code-block:: ini + + [change] + anchor = char-class-escape + heading = Character-class escaping is the minimal correct form + before = any_of_chars('#?!@$%^&*-') -> [\#\?!@$%\^\&\*\-] + after = any_of_chars('#?!@$%^&*-') -> [#?!@$%^&*-] + context = Only backslash, closing bracket, first-position caret, and interior + dash are escaped inside a character class; every other metacharacter is a + literal. Match behavior is unchanged. + +Fields +------ + +``anchor`` (required) + The frozen ``.. _<anchor>:`` label the upgrade-guide section carries. The + deprecation-warning URL for this change ends in ``#<anchor>``. + +``heading`` (required) + The one-line human title of the change. + +``before`` / ``after`` (optional) + A before/after pair. Include both or neither. Rendered as a literal block when + present. + +``context`` (required) + One or two sentences explaining what changed and what the caller should do. + May span multiple indented lines. + +Release flow +------------ + +On release, ``tools/changes.py`` slots each fragment into the matching section of +``CHANGELOG.rst`` and the active upgrade guide in fragment-id order, then removes +the fragment files. Adding a fragment is the only step a contributor takes; the +generator does the rest. diff --git a/tests/discipline/anchors.test.py b/tests/discipline/anchors.test.py new file mode 100644 index 0000000..0e5e8d7 --- /dev/null +++ b/tests/discipline/anchors.test.py @@ -0,0 +1,66 @@ +"""Deprecation-URL anchor contract. + +A deprecation warning points users at ``docs/upgrading/...#anchor``. Shape-checking +the URL alone passes even when the anchor 404s. This test extracts every anchor +referenced by a deprecation stub, parses the upgrade guides for their actual +``.. _label:`` targets, and asserts each referenced anchor exists. + +Edify 1.0 ships stub-free, so the deprecation-URL registry is empty today; the +test still verifies that every anchor the upgrade guides *declare* is well-formed +and unique, so the contract is enforced the moment the first stub lands. +""" + +import re +from pathlib import Path + +_REPO_ROOT = Path(__file__).parent.parent.parent +_UPGRADING_DIR = _REPO_ROOT / "docs" / "upgrading" +_ANCHOR_PATTERN = re.compile(r"^\.\.\s+_([a-zA-Z0-9][a-zA-Z0-9\-]*):\s*$") + +# Anchors referenced by deprecation-warning URLs. Each entry is (guide-stem, anchor). +# Empty in 1.0 (stub-free); every future deprecation stub adds its (guide, anchor) row. +_DEPRECATION_URL_ANCHORS: frozenset[tuple[str, str]] = frozenset() + + +def _declared_anchors(rst_path: Path) -> list[str]: + anchors: list[str] = [] + for line in rst_path.read_text().splitlines(): + matched = _ANCHOR_PATTERN.match(line) + if matched is not None: + anchors.append(matched.group(1)) + return anchors + + +def _anchors_by_guide() -> dict[str, list[str]]: + result: dict[str, list[str]] = {} + for rst_path in sorted(_UPGRADING_DIR.glob("*.rst")): + result[rst_path.stem] = _declared_anchors(rst_path) + return result + + +def test_upgrade_guide_anchors_are_unique_within_each_guide(): + for guide_stem, anchors in _anchors_by_guide().items(): + assert len(anchors) == len(set(anchors)), ( + f"duplicate anchor(s) in docs/upgrading/{guide_stem}.rst: " + f"{sorted({a for a in anchors if anchors.count(a) > 1})}" + ) + + +def test_every_deprecation_url_anchor_exists_in_its_guide(): + anchors_by_guide = _anchors_by_guide() + for guide_stem, anchor in _DEPRECATION_URL_ANCHORS: + declared = anchors_by_guide.get(guide_stem, []) + assert anchor in declared, ( + f"deprecation URL references #{anchor} in docs/upgrading/{guide_stem}.rst, " + f"but no `.. _{anchor}:` target exists there." + ) + + +def test_the_0_3_to_1_0_guide_declares_the_license_anchor(): + anchors = _anchors_by_guide()["0.3-to-1.0"] + assert "license-mit" in anchors + + +def test_the_0_3_to_1_0_guide_declares_the_python_floor_anchor(): + anchors = _anchors_by_guide()["0.3-to-1.0"] + assert "python-floor" in anchors diff --git a/tests/discipline/deprecations.test.py b/tests/discipline/deprecations.test.py new file mode 100644 index 0000000..fe3416e --- /dev/null +++ b/tests/discipline/deprecations.test.py @@ -0,0 +1,72 @@ +"""Deprecation-stub contract harness. + +Every deprecated public symbol must fire exactly one :class:`DeprecationWarning` +whose message matches the policy format:: + + <name> is deprecated since <version>; use <replacement>. See <docs-url> + +with ``stacklevel=2`` so the warning points at the caller. Edify 1.0 ships +stub-free, so the registry below is empty; this harness parametrizes over it, so +the moment a stub lands its row here pins the category, message shape, single-fire +behavior, and documentation URL. +""" + +import importlib +import re +import warnings + +import pytest + +_MESSAGE_PATTERN = re.compile( + r"^(?P<name>\S+) is deprecated since (?P<version>\S+); " + r"use (?P<replacement>.+)\. See (?P<url>https://\S+#\S+)$" +) + +# One row per deprecated stub: (import_path, symbol_name, call_args). +# Empty in 1.0 (stub-free). Each future deprecation adds its row here. +_DEPRECATED_STUBS: list[tuple[str, str, tuple[object, ...]]] = [] + + + ("import_path", "symbol_name", "call_args"), + _DEPRECATED_STUBS, + ids=[f"{path}.{name}" for path, name, _ in _DEPRECATED_STUBS], +) +def test_deprecated_stub_fires_one_well_formed_warning(import_path, symbol_name, call_args): + module = importlib.import_module(import_path) + stub = getattr(module, symbol_name) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + stub(*call_args) + deprecation_warnings = [w for w in caught if issubclass(w.category, DeprecationWarning)] + assert len(deprecation_warnings) == 1 + message_text = str(deprecation_warnings[0].message) + assert _MESSAGE_PATTERN.match(message_text), ( + f"{import_path}.{symbol_name} warning does not match the policy format: {message_text!r}" + ) + + +def test_message_pattern_accepts_a_policy_conformant_message(): + conformant = ( + "old_name is deprecated since 1.1; use new_name. " + "See https://edify.readthedocs.io/en/latest/upgrading/1.0-to-1.1.html#new-name" + ) + assert _MESSAGE_PATTERN.match(conformant) + + +def test_message_pattern_rejects_a_message_missing_the_docs_url(): + non_conformant = "old_name is deprecated since 1.1; use new_name." + assert _MESSAGE_PATTERN.match(non_conformant) is None + + +def test_message_pattern_rejects_a_url_without_an_anchor(): + non_conformant = ( + "old_name is deprecated since 1.1; use new_name. " + "See https://edify.readthedocs.io/en/latest/upgrading/1.0-to-1.1.html" + ) + assert _MESSAGE_PATTERN.match(non_conformant) is None + + +def test_registry_is_empty_or_every_row_has_three_fields(): + for row in _DEPRECATED_STUBS: + assert len(row) == 3 diff --git a/tests/docs/snapshots.test.py b/tests/docs/snapshots.test.py index b1c223b..f335373 100644 --- a/tests/docs/snapshots.test.py +++ b/tests/docs/snapshots.test.py @@ -103,6 +103,12 @@ _BLOCKS_DEFERRED_TO_DOCS_REWRITE = frozenset( } ) +_ILLUSTRATIVE_NON_EXECUTABLE_BLOCKS = frozenset( + { + ("upgrading/0.3-to-1.0", 140), + } +) + _BLOCKS_SKIPPED_ON_PYPY = frozenset( { ("regex-builder/flags/index", 38), @@ -123,6 +129,8 @@ def test_doc_code_block_produces_the_snapshotted_regex( stem_string = str(relative_stem) if (stem_string, block_start) in _BLOCKS_DEFERRED_TO_DOCS_REWRITE: pytest.skip("doc block references validators / kwargs slated for the docs rewrite") + if (stem_string, block_start) in _ILLUSTRATIVE_NON_EXECUTABLE_BLOCKS: + pytest.skip("doc block shows pre/post-migration code that is intentionally not executable") if _ON_PYPY and (stem_string, block_start) in _BLOCKS_SKIPPED_ON_PYPY: pytest.skip("doc block hits PyPy's re.DEBUG upstream disassembler bug") namespace = _prepared_exec_namespace() diff --git a/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex b/tests/snapshots/docs/deprecation-policy/0020.regex index e69de29..e69de29 100644 --- a/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex +++ b/tests/snapshots/docs/deprecation-policy/0020.regex diff --git a/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex b/tests/snapshots/docs/upgrading/0.3-to-1.0/0046.regex index e69de29..e69de29 100644 --- a/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex +++ b/tests/snapshots/docs/upgrading/0.3-to-1.0/0046.regex diff --git a/tests/snapshots/docs/upgrading/0.3-to-1.0/0071.regex b/tests/snapshots/docs/upgrading/0.3-to-1.0/0071.regex new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/snapshots/docs/upgrading/0.3-to-1.0/0071.regex diff --git a/tests/tools/changes.test.py b/tests/tools/changes.test.py new file mode 100644 index 0000000..7eeb777 --- /dev/null +++ b/tests/tools/changes.test.py @@ -0,0 +1,116 @@ +"""Tests for the ``changes/`` fragment generator in ``tools/changes.py``.""" + +import importlib.util +from pathlib import Path + +_REPO_ROOT = Path(__file__).parent.parent.parent +_GENERATOR_PATH = _REPO_ROOT / "tools" / "changes.py" + + +def _load_generator(): + spec = importlib.util.spec_from_file_location("edify_changes_tool", _GENERATOR_PATH) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +_GENERATOR = _load_generator() + + +def _write_fragment(directory: Path, name: str, body: str) -> Path: + fragment_path = directory / name + fragment_path.write_text(body, encoding="utf-8") + return fragment_path + + +def test_collect_fragments_returns_files_in_fragment_id_order(tmp_path): + _write_fragment(tmp_path, "0020-second.rst", "[change]\nanchor=b\nheading=B\ncontext=b") + _write_fragment(tmp_path, "0010-first.rst", "[change]\nanchor=a\nheading=A\ncontext=a") + collected = _GENERATOR.collect_fragments(tmp_path) + assert [path.name for path in collected] == ["0010-first.rst", "0020-second.rst"] + + +def test_collect_fragments_skips_the_readme(tmp_path): + _write_fragment(tmp_path, "README.rst", "not a fragment") + _write_fragment(tmp_path, "0010-only.rst", "[change]\nanchor=a\nheading=A\ncontext=a") + collected = _GENERATOR.collect_fragments(tmp_path) + assert [path.name for path in collected] == ["0010-only.rst"] + + +def test_render_fragment_emits_anchor_heading_and_context(tmp_path): + fragment_path = _write_fragment( + tmp_path, + "0010-example.rst", + "[change]\nanchor = my-anchor\nheading = My Heading\ncontext = A single sentence.", + ) + rendered = _GENERATOR.render_fragment(fragment_path) + assert ".. _my-anchor:" in rendered + assert "My Heading" in rendered + assert "-" * len("My Heading") in rendered + assert "A single sentence." in rendered + + +def test_render_fragment_collapses_multiline_context_into_one_paragraph(tmp_path): + fragment_path = _write_fragment( + tmp_path, + "0010-multiline.rst", + "[change]\nanchor = a\nheading = H\ncontext = first line\n second line", + ) + rendered = _GENERATOR.render_fragment(fragment_path) + assert "first line second line" in rendered + + +def test_render_fragment_includes_before_after_block_when_both_present(tmp_path): + fragment_path = _write_fragment( + tmp_path, + "0010-beforeafter.rst", + "[change]\nanchor = a\nheading = H\nbefore = old\nafter = new\ncontext = changed", + ) + rendered = _GENERATOR.render_fragment(fragment_path) + assert ".. code-block:: text" in rendered + assert " old" in rendered + assert " new" in rendered + + +def test_render_fragment_omits_before_after_block_when_absent(tmp_path): + fragment_path = _write_fragment( + tmp_path, + "0010-nofix.rst", + "[change]\nanchor = a\nheading = H\ncontext = no before/after here", + ) + rendered = _GENERATOR.render_fragment(fragment_path) + assert ".. code-block:: text" not in rendered + + +def test_render_all_concatenates_every_fragment_in_order(tmp_path): + _write_fragment(tmp_path, "0020-b.rst", "[change]\nanchor=b\nheading=Bravo\ncontext=b") + _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=Alpha\ncontext=a") + rendered = _GENERATOR.render_all(tmp_path) + assert rendered.index("Alpha") < rendered.index("Bravo") + + +def test_main_release_deletes_consumed_fragments(tmp_path, monkeypatch): + _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=A\ncontext=a") + monkeypatch.setattr(_GENERATOR, "_CHANGES_DIR", tmp_path) + exit_code = _GENERATOR.main(["--release"]) + assert exit_code == 0 + assert _GENERATOR.collect_fragments(tmp_path) == [] + + +def test_main_without_release_leaves_fragments_in_place(tmp_path, monkeypatch, capsys): + _write_fragment(tmp_path, "0010-a.rst", "[change]\nanchor=a\nheading=A\ncontext=a") + monkeypatch.setattr(_GENERATOR, "_CHANGES_DIR", tmp_path) + exit_code = _GENERATOR.main([]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "A" in captured.out + assert len(_GENERATOR.collect_fragments(tmp_path)) == 1 + + +def test_committed_changes_directory_fragments_all_parse(): + changes_dir = _REPO_ROOT / "changes" + for fragment_path in _GENERATOR.collect_fragments(changes_dir): + rendered = _GENERATOR.render_fragment(fragment_path) + assert rendered.strip() != "" diff --git a/tests/tools/surface.test.py b/tests/tools/surface.test.py new file mode 100644 index 0000000..0b8daf0 --- /dev/null +++ b/tests/tools/surface.test.py @@ -0,0 +1,89 @@ +"""Tests for the public-surface snapshot tool in ``tools/surface.py``.""" + +import importlib.util +from pathlib import Path + +_REPO_ROOT = Path(__file__).parent.parent.parent +_TOOL_PATH = _REPO_ROOT / "tools" / "surface.py" +_SURFACE_PATH = _REPO_ROOT / ".public-surface" + + +def _load_tool(): + spec = importlib.util.spec_from_file_location("edify_surface_tool", _TOOL_PATH) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +_TOOL = _load_tool() + + +def test_committed_surface_file_exists_and_is_non_empty(): + assert _SURFACE_PATH.exists() + assert _SURFACE_PATH.read_text(encoding="utf-8").strip() != "" + + +def test_computed_surface_matches_the_committed_snapshot(): + computed = _TOOL.compute_surface() + committed = _SURFACE_PATH.read_text(encoding="utf-8") + assert computed == committed, ( + "public surface drift: run `python tools/surface.py --write` and commit " + ".public-surface with a changes/ fragment describing the change." + ) + + +def test_surface_lists_the_core_public_symbols(): + computed = _TOOL.compute_surface() + assert "edify.Pattern" in computed + assert "edify.RegexBuilder" in computed + assert "edify.Regex" in computed + assert "edify.EdifyError" in computed + + +def test_surface_excludes_private_module_paths(): + computed = _TOOL.compute_surface() + for line in computed.splitlines(): + dotted = line.split("(")[0] + parts = dotted.split(".") + assert not any(part.startswith("_") for part in parts[:-1]), ( + f"surface line leaks a private module path: {line!r}" + ) + + +def test_surface_is_sorted_and_deduplicated(): + computed = _TOOL.compute_surface() + lines = computed.splitlines() + assert lines == sorted(lines) + assert len(lines) == len(set(lines)) + + +def test_check_returns_zero_when_surface_matches(capsys): + exit_code = _TOOL.main(["--check"]) + assert exit_code == 0 + + +def test_check_returns_one_when_surface_drifts(tmp_path, monkeypatch, capsys): + drifted_snapshot = tmp_path / ".public-surface" + drifted_snapshot.write_text("edify.SomethingRemoved\n", encoding="utf-8") + monkeypatch.setattr(_TOOL, "_SURFACE_PATH", drifted_snapshot) + exit_code = _TOOL.main(["--check"]) + captured = capsys.readouterr() + assert exit_code == 1 + assert "public surface drift" in captured.err + + +def test_write_overwrites_the_snapshot_file(tmp_path, monkeypatch): + target = tmp_path / ".public-surface" + monkeypatch.setattr(_TOOL, "_SURFACE_PATH", target) + exit_code = _TOOL.main(["--write"]) + assert exit_code == 0 + assert target.read_text(encoding="utf-8") == _TOOL.compute_surface() + + +def test_bare_invocation_prints_the_surface_to_stdout(capsys): + exit_code = _TOOL.main([]) + captured = capsys.readouterr() + assert exit_code == 0 + assert "edify.Pattern" in captured.out diff --git a/tools/changes.py b/tools/changes.py new file mode 100644 index 0000000..0c3e0bf --- /dev/null +++ b/tools/changes.py @@ -0,0 +1,65 @@ +"""Render committed change fragments into CHANGELOG / upgrade-guide RST. + +Stdlib only, no external changelog tooling. Reads every ``changes/*.rst`` +fragment (INI with a single ``[change]`` section), sorts them by filename, and +renders each into an RST section. On ``--release`` the rendered blocks are +slotted into the target files and the consumed fragments are deleted. +""" + +from __future__ import annotations + +import configparser +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_CHANGES_DIR = _REPO_ROOT / "changes" + + +def collect_fragments(changes_dir: Path) -> list[Path]: + """Return the fragment files under ``changes_dir`` in fragment-id order.""" + fragments = [path for path in changes_dir.glob("*.rst") if path.name != "README.rst"] + return sorted(fragments, key=lambda path: path.name) + + +def render_fragment(fragment_path: Path) -> str: + """Return the RST block for a single fragment file.""" + parser = configparser.ConfigParser() + parser.read(fragment_path, encoding="utf-8") + change = parser["change"] + anchor = change["anchor"] + heading = change["heading"] + context = _normalize_paragraph(change["context"]) + lines = [f".. _{anchor}:", "", heading, "-" * len(heading), "", context, ""] + before = change.get("before") + after = change.get("after") + if before is not None and after is not None: + lines.extend(["", ".. code-block:: text", "", f" {before}", f" {after}", ""]) + return "\n".join(lines) + + +def render_all(changes_dir: Path) -> str: + """Return the concatenated RST for every fragment under ``changes_dir``.""" + fragment_paths = collect_fragments(changes_dir) + rendered_blocks = [render_fragment(path) for path in fragment_paths] + return "\n".join(rendered_blocks) + + +def _normalize_paragraph(raw: str) -> str: + collapsed_lines = [line.strip() for line in raw.splitlines()] + non_empty = [line for line in collapsed_lines if line] + return " ".join(non_empty) + + +def main(argv: list[str]) -> int: + """Print the rendered fragments; with ``--release`` also delete them.""" + rendered = render_all(_CHANGES_DIR) + sys.stdout.write(rendered) + if "--release" in argv: + for fragment_path in collect_fragments(_CHANGES_DIR): + fragment_path.unlink() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/surface.py b/tools/surface.py new file mode 100644 index 0000000..2677d95 --- /dev/null +++ b/tools/surface.py @@ -0,0 +1,99 @@ +"""Compute Edify's public API surface as a stable, diffable text snapshot. + +Walks ``edify`` and its documented public submodules, and emits one line per +public symbol: its dotted path plus, for callables, a normalized signature. The +output is deterministic (sorted) so a diff against the committed +``.public-surface`` file shows exactly what a PR added, removed, or changed. +""" + +from __future__ import annotations + +import importlib +import inspect +import pkgutil +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parent.parent +_SURFACE_PATH = _REPO_ROOT / ".public-surface" +_ROOT_PACKAGE = "edify" + + +def compute_surface() -> str: + """Return the full public-surface snapshot text for the ``edify`` package.""" + module_names = _public_module_names() + entries: list[str] = [] + for module_name in module_names: + module = importlib.import_module(module_name) + entries.extend(_module_entries(module_name, module)) + unique_sorted = sorted(set(entries)) + return "\n".join(unique_sorted) + "\n" + + +def _public_module_names() -> list[str]: + root_module = importlib.import_module(_ROOT_PACKAGE) + root_path = root_module.__path__ + names = [_ROOT_PACKAGE] + for module_info in pkgutil.walk_packages(root_path, prefix=f"{_ROOT_PACKAGE}."): + if _is_private_path(module_info.name): + continue + names.append(module_info.name) + return names + + +def _is_private_path(dotted_name: str) -> bool: + parts = dotted_name.split(".") + return any(part.startswith("_") for part in parts) + + +def _module_entries(module_name: str, module: object) -> list[str]: + exported_names = getattr(module, "__all__", None) + if exported_names is None: + return [] + entries: list[str] = [] + for symbol_name in exported_names: + if symbol_name.startswith("_"): + continue + value = getattr(module, symbol_name, None) + entries.append(_render_symbol(module_name, symbol_name, value)) + return entries + + +def _render_symbol(module_name: str, symbol_name: str, value: object) -> str: + dotted = f"{module_name}.{symbol_name}" + signature = _signature_or_empty(value) + if signature: + return f"{dotted}{signature}" + return dotted + + +def _signature_or_empty(value: object) -> str: + if not callable(value): + return "" + try: + return str(inspect.signature(value)) + except (ValueError, TypeError): + return "" + + +def main(argv: list[str]) -> int: + """Print the surface; with ``--write`` overwrite the committed snapshot file.""" + surface = compute_surface() + if "--write" in argv: + _SURFACE_PATH.write_text(surface, encoding="utf-8") + return 0 + if "--check" in argv: + committed = _SURFACE_PATH.read_text(encoding="utf-8") + if committed != surface: + sys.stderr.write( + "public surface drift: run `python tools/surface.py --write` and commit " + ".public-surface, adding a changes/ fragment for the change.\n" + ) + return 1 + return 0 + sys.stdout.write(surface) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) |
