From 4940bf5577b341fcfd03cf0952ffcafeff6087e5 Mon Sep 17 00:00:00 2001 From: The Metaist Date: Sun, 31 Dec 2023 18:49:22 -0500 Subject: [PATCH] update: LTS (closes #14) --- .cspell.json | 1 + .github/workflows/ci.yaml | 21 +++--- .github/workflows/codeql-analysis.yml | 72 ++++-------------- .github/workflows/pypi.yaml | 55 +++++++------- .gitignore | 2 +- CONTRIBUTING.md | 89 ++++++++++++++++++++++ README.md | 24 +++--- docs/.nojekyll | 0 docs/attrdict.html | 104 +++++++++++++------------- docs/config.html | 5 +- docs/env.html | 15 ++-- docs/fn.html | 5 +- docs/index.html | 103 +++++++++++++------------ docs/jsend.html | 10 +-- otter-box.png | Bin 0 -> 507333 bytes pyproject.toml | 6 +- src/attrbox/__init__.py | 2 +- 17 files changed, 287 insertions(+), 227 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/.nojekyll create mode 100644 otter-box.png diff --git a/.cspell.json b/.cspell.json index c7abe9c..74a443a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -14,6 +14,7 @@ "mypy", "optvar", "pdoc", + "pyright", "setuptools", "startswith", "tomli" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c15b27f..0dfd5f4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,28 +10,23 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Set up caches - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.cache/pip key: ${{ runner.os }}-py${{ matrix.python-version }} - - name: Checkout repo - uses: actions/checkout@v2 - with: - fetch-depth: 3 - - - name: Fetch tags - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Install dependencies run: | python -m pip install --upgrade pip setuptools wheel @@ -49,6 +44,10 @@ jobs: run: | pdm run mypy + # - name: Type check (pyright) + # run: | + # pdm run pyright + - name: Run tests run: | pdm run test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e36c199..92dab7f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,24 +1,11 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: push: - branches: [ main ] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [ main ] - # schedule: - # - cron: '35 7 * * 3' + branches: [main] jobs: analyze: @@ -32,51 +19,22 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: ["python"] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v2 - with: - python-version: '3.x' + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - setup-python-dependencies: false - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index 069729f..5f6310c 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -5,34 +5,33 @@ on: types: [published] jobs: - deploy: - + pypi-publish: runs-on: ubuntu-latest + permissions: + id-token: write + steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - - name: Check setup.py - run: | - pip install -e . - - - name: Build package - run: | - python -m build - - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install build + + - name: Check install + run: | + pip install -e . + + - name: Build package + run: | + python -m build + + - name: Publish package + uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf diff --git a/.gitignore b/.gitignore index e3ca062..0b3e0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ _seed.py __pycache__ # test -.coverage* +.coverage .mypy_cache .pytest_cache .ruff_cache diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..77c71b8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing + +## Local Development + +```bash +# get the code +git clone git@github.com:metaist/attrbox.git +cd attrbox + +# create a virtual environment +python -m venv .venv --prompt attrbox +. .venv/bin/activate +pip install --upgrade pip + +# install dependencies and dev tools +pip install -e ".[dev]" +pnpm install -g cspell +``` + +As you work on the code, you should periodically run: + +```bash +pdm lint # for type checks +pdm test # for unit tests +``` + +This repo generally tries to maintain type-correctness (via `mypy` and `pyright`) and complete unit test coverage. + +## Making a Release + +Checkout `prod`: + +```bash +git checkout prod +git merge --no-ff --no-edit main +``` + +Update top-most `__init__.py`: + +```python +__version__ = "X.0.1" +``` + +Update `CHANGELOG.md`: + +Sections order is: `Fixed`, `Changed`, `Added`, `Deprecated`, `Removed`, `Security`. + +```markdown +--- + +[X.0.1]: https://github.com/metaist/attrbox/compare/X.0.0...X.0.1 + +## [X.0.1] - XXXX-XX-XXT00:00:00Z + +**Fixed** + +**Changed** + +**Added** + +**Deprecated** + +**Removed** + +**Security** +``` + +### + +```bash +export VER="X.0.1" + +# update docs +pdm docs + +# check build +pip install -e . + +# commit and push tags +git commit -am "release: $VER" +git tag $VER +git push +git push --tags +git checkout main +git merge --no-ff --no-edit prod +git push +``` + +[Create the release on GitHub](https://github.com/metaist/attrbox/releases/new). The `pypi.yaml` workflow will attempt to publish it to PyPI. diff --git a/README.md b/README.md index 328c6ad..20e9bfa 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,14 @@ -# attrbox - -_Attribute-based data structures._ - -[![Build Status](https://img.shields.io/github/actions/workflow/status/metaist/attrbox/.github/workflows/ci.yaml?branch=main&style=for-the-badge)](https://github.com/metaist/attrbox/actions) -[![attrbox on PyPI](https://img.shields.io/pypi/v/attrbox.svg?color=blue&style=for-the-badge)](https://pypi.org/project/attrbox) -[![Supported Python versions](https://img.shields.io/pypi/pyversions/attrbox?style=for-the-badge)](https://pypi.org/project/attrbox) - -[Changelog] - [Issues] - [Documentation] - -[changelog]: https://github.com/metaist/attrbox/blob/main/CHANGELOG.md -[issues]: https://github.com/metaist/attrbox/issues -[documentation]: https://metaist.github.io/attrbox/ +# attrbox: attribute-based data structures + +

+ Otto the Otter
+ Otto the Otter +

+

+ Build + PyPI + Supported Python Versions +

## Why? diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/attrdict.html b/docs/attrdict.html index c176520..614ddff 100644 --- a/docs/attrdict.html +++ b/docs/attrdict.html @@ -80,16 +80,16 @@

Module attrbox.attrdict

""" return self.__class__(super().copy()) - def __contains__(self, name: Any) -> bool: - """Return `True` if `name` is a key. + def __contains__(self, key: Any) -> bool: + """Return `True` if `key` is a key. Args: - name (Any): typically a `AnyIndex`. If it's a string, + key (Any): typically a `AnyIndex`. If it's a string, the check proceeds as usual. If it's a `Sequence`, the checks are performed using `.get()`. Returns: - bool: `True` if the `name` is a valid key, `False` otherwise. + bool: `True` if the `key` is a valid key, `False` otherwise. Examples: Normal checking works as expected: @@ -106,7 +106,7 @@

Module attrbox.attrdict

>>> ['a', 1, 'x'] in items False """ - return self.get(name, NOT_FOUND) is not NOT_FOUND + return self.get(key, NOT_FOUND) is not NOT_FOUND def __getattr__(self, name: str) -> Optional[Any]: """Return the value of the attribute or `None`. @@ -206,11 +206,11 @@

Module attrbox.attrdict

except AttributeError: # use key/value del self[name] - def __getitem__(self, name: AnyIndex) -> Optional[Any]: + def __getitem__(self, key: AnyIndex) -> Optional[Any]: """Return the value of the key. Args: - name (AnyIndex): key name or Sequence of the path to a key + key (AnyIndex): key name or Sequence of the path to a key Returns: Any: value of the key or `None` if it cannot be found @@ -222,13 +222,13 @@

Module attrbox.attrdict

>>> item['b'] is None True """ - return self.get(name) + return self.get(key) - def __setitem__(self, name: AnyIndex, value: Any) -> None: + def __setitem__(self, key: AnyIndex, value: Any) -> None: """Set the value of a key. Args: - name (BoxIndex): key name + key (AnyIndex): key name value (Any): key value Examples: @@ -241,13 +241,13 @@

Module attrbox.attrdict

>>> item.a.b 10 """ - self.set(name, value) + self.set(key, value) - def __delitem__(self, name: str) -> None: + def __delitem__(self, key: str) -> None: """Delete a key. Args: - name (str): key name + key (str): key name Examples: >>> item = AttrDict(a=1) @@ -262,7 +262,7 @@

Module attrbox.attrdict

{'a': 1} """ try: - super().__delitem__(name) + super().__delitem__(key) except KeyError: pass @@ -427,16 +427,16 @@

Examples

""" return self.__class__(super().copy()) - def __contains__(self, name: Any) -> bool: - """Return `True` if `name` is a key. + def __contains__(self, key: Any) -> bool: + """Return `True` if `key` is a key. Args: - name (Any): typically a `AnyIndex`. If it's a string, + key (Any): typically a `AnyIndex`. If it's a string, the check proceeds as usual. If it's a `Sequence`, the checks are performed using `.get()`. Returns: - bool: `True` if the `name` is a valid key, `False` otherwise. + bool: `True` if the `key` is a valid key, `False` otherwise. Examples: Normal checking works as expected: @@ -453,7 +453,7 @@

Examples

>>> ['a', 1, 'x'] in items False """ - return self.get(name, NOT_FOUND) is not NOT_FOUND + return self.get(key, NOT_FOUND) is not NOT_FOUND def __getattr__(self, name: str) -> Optional[Any]: """Return the value of the attribute or `None`. @@ -553,11 +553,11 @@

Examples

except AttributeError: # use key/value del self[name] - def __getitem__(self, name: AnyIndex) -> Optional[Any]: + def __getitem__(self, key: AnyIndex) -> Optional[Any]: """Return the value of the key. Args: - name (AnyIndex): key name or Sequence of the path to a key + key (AnyIndex): key name or Sequence of the path to a key Returns: Any: value of the key or `None` if it cannot be found @@ -569,13 +569,13 @@

Examples

>>> item['b'] is None True """ - return self.get(name) + return self.get(key) - def __setitem__(self, name: AnyIndex, value: Any) -> None: + def __setitem__(self, key: AnyIndex, value: Any) -> None: """Set the value of a key. Args: - name (BoxIndex): key name + key (AnyIndex): key name value (Any): key value Examples: @@ -588,13 +588,13 @@

Examples

>>> item.a.b 10 """ - self.set(name, value) + self.set(key, value) - def __delitem__(self, name: str) -> None: + def __delitem__(self, key: str) -> None: """Delete a key. Args: - name (str): key name + key (str): key name Examples: >>> item = AttrDict(a=1) @@ -609,7 +609,7 @@

Examples

{'a': 1} """ try: - super().__delitem__(name) + super().__delitem__(key) except KeyError: pass @@ -758,13 +758,13 @@

Examples

-def __contains__(self, name: Any) ‑> bool +def __contains__(self, key: Any) ‑> bool
-

Return True if name is a key.

+

Return True if key is a key.

Args

-
name : Any
+
key : Any
typically a AnyIndex. If it's a string, the check proceeds as usual. If it's a Sequence, the checks are performed using .get().
@@ -772,7 +772,7 @@

Args

Returns

bool
-
True if the name is a valid key, False otherwise.
+
True if the key is a valid key, False otherwise.

Examples

Normal checking works as expected:

@@ -793,16 +793,16 @@

Examples

Expand source code -
def __contains__(self, name: Any) -> bool:
-    """Return `True` if `name` is a key.
+
def __contains__(self, key: Any) -> bool:
+    """Return `True` if `key` is a key.
 
     Args:
-        name (Any): typically a `AnyIndex`. If it's a string,
+        key (Any): typically a `AnyIndex`. If it's a string,
             the check proceeds as usual. If it's a `Sequence`, the
             checks are performed using `.get()`.
 
     Returns:
-        bool: `True` if the `name` is a valid key, `False` otherwise.
+        bool: `True` if the `key` is a valid key, `False` otherwise.
 
     Examples:
         Normal checking works as expected:
@@ -819,7 +819,7 @@ 

Examples

>>> ['a', 1, 'x'] in items False """ - return self.get(name, NOT_FOUND) is not NOT_FOUND
+ return self.get(key, NOT_FOUND) is not NOT_FOUND
@@ -1034,13 +1034,13 @@

Examples

-def __getitem__(self, name: AnyIndex) ‑> Optional[Any] +def __getitem__(self, key: AnyIndex) ‑> Optional[Any]

Return the value of the key.

Args

-
name : AnyIndex
+
key : AnyIndex
key name or Sequence of the path to a key

Returns

@@ -1059,11 +1059,11 @@

Examples

Expand source code -
def __getitem__(self, name: AnyIndex) -> Optional[Any]:
+
def __getitem__(self, key: AnyIndex) -> Optional[Any]:
     """Return the value of the key.
 
     Args:
-        name (AnyIndex): key name or Sequence of the path to a key
+        key (AnyIndex): key name or Sequence of the path to a key
 
     Returns:
         Any: value of the key or `None` if it cannot be found
@@ -1075,17 +1075,17 @@ 

Examples

>>> item['b'] is None True """ - return self.get(name)
+ return self.get(key)
-def __setitem__(self, name: AnyIndex, value: Any) ‑> None +def __setitem__(self, key: AnyIndex, value: Any) ‑> None

Set the value of a key.

Args

-
name : BoxIndex
+
key : AnyIndex
key name
value : Any
key value
@@ -1104,11 +1104,11 @@

Examples

Expand source code -
def __setitem__(self, name: AnyIndex, value: Any) -> None:
+
def __setitem__(self, key: AnyIndex, value: Any) -> None:
     """Set the value of a key.
 
     Args:
-        name (BoxIndex): key name
+        key (AnyIndex): key name
         value (Any): key value
 
     Examples:
@@ -1121,17 +1121,17 @@ 

Examples

>>> item.a.b 10 """ - self.set(name, value)
+ self.set(key, value)
-def __delitem__(self, name: str) ‑> None +def __delitem__(self, key: str) ‑> None

Delete a key.

Args

-
name : str
+
key : str
key name

Examples

@@ -1150,11 +1150,11 @@

Examples

Expand source code -
def __delitem__(self, name: str) -> None:
+
def __delitem__(self, key: str) -> None:
     """Delete a key.
 
     Args:
-        name (str): key name
+        key (str): key name
 
     Examples:
         >>> item = AttrDict(a=1)
@@ -1169,7 +1169,7 @@ 

Examples

{'a': 1} """ try: - super().__delitem__(name) + super().__delitem__(key) except KeyError: pass
diff --git a/docs/config.html b/docs/config.html index 2a41d43..5dacc4b 100644 --- a/docs/config.html +++ b/docs/config.html @@ -36,6 +36,7 @@

Module attrbox.config

from typing import Callable from typing import Dict from typing import List +from typing import LiteralString from typing import Mapping from typing import Optional from typing import Sequence @@ -51,7 +52,7 @@

Module attrbox.config

from . import env PYTHON_KEYWORDS: List[ - str + LiteralString ] = """\ False await else import pass None break except in raise @@ -300,7 +301,7 @@

Module attrbox.config

Global variables

-
var PYTHON_KEYWORDS : List[str]
+
var PYTHON_KEYWORDS : List[LiteralString]
diff --git a/docs/env.html b/docs/env.html index f45477a..00a0420 100644 --- a/docs/env.html +++ b/docs/env.html @@ -113,6 +113,7 @@

Examples

def read(self) -> str: """Read the contents of the file-like object.""" + return "" # pragma: no cover def expand( @@ -175,7 +176,7 @@

Examples

name = name.split(".") if name in values: - value = str(values[name]) + value = str(values[name]) # pyright: ignore return value return _RE_EXPAND.sub(_repl, value) @@ -478,7 +479,7 @@

Examples

name = name.split(".") if name in values: - value = str(values[name]) + value = str(values[name]) # pyright: ignore return value return _RE_EXPAND.sub(_repl, value)
@@ -658,7 +659,7 @@

Examples

-def find_env(path: Union[pathlib.Path, str, None] = None, name: str = '.env') ‑> Optional[pathlib.Path] +def find_env(path: Union[pathlib.Path, str, ForwardRef(None)] = None, name: str = '.env') ‑> Optional[pathlib.Path]

Find the .env file in the ancestors of the current path.

@@ -740,7 +741,7 @@

Examples

-def load_env(path: Union[pathlib.Path, str, None] = None) ‑> Dict[str, str] +def load_env(path: Union[pathlib.Path, str, ForwardRef(None)] = None) ‑> Dict[str, str]

Load an environment file.

@@ -827,7 +828,8 @@

Classes

"""Protocol for a class that implements a `.read()` method.""" def read(self) -> str: - """Read the contents of the file-like object."""
+ """Read the contents of the file-like object.""" + return "" # pragma: no cover

Ancestors

    @@ -846,7 +848,8 @@

    Methods

    Expand source code
    def read(self) -> str:
    -    """Read the contents of the file-like object."""
    + """Read the contents of the file-like object.""" + return "" # pragma: no cover
diff --git a/docs/fn.html b/docs/fn.html index 3d5473c..5eb635f 100644 --- a/docs/fn.html +++ b/docs/fn.html @@ -64,6 +64,7 @@

Module attrbox.fn

def __contains__(self, key: Any) -> bool: """Return `True` if `key` exists, `False` otherwise.""" + return False def __getitem__(self, key: Any) -> Any: """Return value of `key`.""" @@ -516,11 +517,13 @@

Classes

Expand source code -
class SupportsItem(Protocol):  # pragma: no cover
+
@runtime_checkable
+class SupportsItem(Protocol):  # pragma: no cover
     """Protocol for `k in x`, `x[k]`, and `x[k] = v`."""
 
     def __contains__(self, key: Any) -> bool:
         """Return `True` if `key` exists, `False` otherwise."""
+        return False
 
     def __getitem__(self, key: Any) -> Any:
         """Return value of `key`."""
diff --git a/docs/index.html b/docs/index.html
index 889f6e7..7d2a563 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -23,10 +23,15 @@ 

Package attrbox

Attribute-based data structures.

-

Build Status -attrbox on PyPI -Supported Python versions

-

Changelog - Issues - Documentation

+

+Otto the Otter
+Otto the Otter +

+

+Build +PyPI +Supported Python Versions +

Why?

I have common use cases where I want to improve python's dict and list: