From 4c012ac16aa72434bba5bb104cf2ce90b303f8a8 Mon Sep 17 00:00:00 2001 From: Steven Murray Date: Tue, 11 Jul 2023 12:17:07 -0700 Subject: [PATCH 1/4] build: use pyproject.toml --- README.md | 2 +- beetsplug/__init__.py | 1 - pyproject.toml | 50 ++++++++++++++++++++++++++++++++++ requirements_dev.txt | 29 -------------------- setup.py | 62 ------------------------------------------- tox.ini | 24 ----------------- 6 files changed, 51 insertions(+), 117 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements_dev.txt delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/README.md b/README.md index 33b7084..f99331d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ that provides the ability to summarize statistics according to fields. ## Installation ``` -$ pip install git+git://github.com/steven-murray/beet-summarize.git +$ pip install git+https://github.com/steven-murray/beet-summarize.git ``` diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py index 07e6e3b..299975c 100644 --- a/beetsplug/__init__.py +++ b/beetsplug/__init__.py @@ -1,4 +1,3 @@ -from .summarize import parse_stat, show_summary, summarize from pkgutil import extend_path __version__ = '0.2.0' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d200087 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] + +[tool.black] +line-length = 88 +py36 = true +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[project] +name = "summarize" +authors = [ + {name = "Steven Murray", email = "steven.g.murray@asu.edu"}, +] +description = "Summarize your beets library" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "MIT"} +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dynamic = ["version"] +urls = [ + "https://github.com/steven-murray/beet-summarize", +] + +[project.optional-dependencies] +tests = ["pytest"] diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 3150d3b..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,29 +0,0 @@ -appdirs==1.4.3 -atomicwrites==1.3.0 -attrs==19.1.0 -beets==1.4.7 -certifi==2019.3.9 -Click==7.0 -coverage==4.5.3 -entrypoints==0.3 -filelock==3.0.10 -flake8==3.7.7 -jellyfish==0.7.1 -mccabe==0.6.1 -more-itertools==7.0.0 -munkres==1.1.2 -musicbrainzngs==0.6 -mutagen==1.42.0 -pluggy==0.11.0 -py==1.8.0 -pycodestyle==2.5.0 -pyflakes==2.1.1 -pytest==4.4.1 -PyYAML==5.1 -six==1.12.0 -toml==0.10.0 -tox==3.9.0 -Unidecode==1.0.23 -virtualenv==16.5.0 -pytest-cov -coveralls \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 9f53985..0000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""The setup script.""" - -import os -import re - -from setuptools import setup, find_packages - - -def find_version(): - with open(os.path.join(os.path.dirname(__file__), "beetsplug", "__init__.py")) as fp: - version_file = fp.read() - - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -with open('README.md') as readme_file: - readme = readme_file.read() - -with open('HISTORY.rst') as history_file: - history = history_file.read() - -requirements = [ -] - -setup_requirements = [ -] - -test_requirements = ['pytest', ] - -setup( - author="Steven Murray", - author_email='steven.g.murray@asu.edu', - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Scientists', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - ], - description="Summarize your beets library", - install_requires=requirements, - license="MIT license", - long_description=readme + '\n\n' + history, - name='summarize', - packages=find_packages(include=['beetsplug']), - setup_requires=setup_requirements, - test_suite='tests', - tests_require=test_requirements, - url='https://github.com/steven-murray/beet-summarize', - version=find_version(), - zip_safe=False, -) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 9e6e896..0000000 --- a/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py35, py36, py37, flake8 - -[travis] -python = - 3.7: py37 - 3.6: py36 - 3.5: py35 - -[testenv:flake8] -basepython = python -deps = flake8 -commands = flake8 summarize - -[testenv] -passenv = TRAVIS TRAVIS_* -setenv = - PYTHONPATH = {toxinidir} -deps = - -r{toxinidir}/requirements_dev.txt -commands = - pip install -U pip - py.test --basetemp={envtmpdir} --cov=summarize - coveralls \ No newline at end of file From 2cbfa03f1546102c0c43e2637d248fc69b778378 Mon Sep 17 00:00:00 2001 From: Steven Murray Date: Tue, 11 Jul 2023 12:50:12 -0700 Subject: [PATCH 2/4] ci: add .github actions --- .flake8 | 28 ++++-- .github/dependabot.yml | 21 +++++ .github/labels.yml | 66 ++++++++++++++ .github/release-drafter.yml | 66 ++++++++++++++ .github/workflows/auto-merge-deps.yml | 14 +++ .github/workflows/check-build.yml | 27 ++++++ .github/workflows/labeler.yml | 18 ++++ .github/workflows/release-draft.yml | 21 +++++ .github/workflows/testsuite.yaml | 41 +++++++++ .pre-commit-config.yaml | 59 ++++++++++++ .travis.yml | 14 --- CONTRIBUTING.rst | 4 +- README.md | 57 ++++++------ beetsplug/__init__.py | 4 +- beetsplug/summarize.py | 123 ++++++++++++++------------ pyproject.toml | 11 ++- tests/test_summarize.py | 4 +- 17 files changed, 462 insertions(+), 116 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/labels.yml create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/auto-merge-deps.yml create mode 100644 .github/workflows/check-build.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/release-draft.yml create mode 100644 .github/workflows/testsuite.yaml create mode 100644 .pre-commit-config.yaml delete mode 100644 .travis.yml diff --git a/.flake8 b/.flake8 index b0635d4..12eb968 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,25 @@ [flake8] -# Recommend matching the black default line length of 88, -# rather than the flake8 default of 79: +ignore = + E203 + T201 max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, \ No newline at end of file +max-complexity = 20 +rst-roles = + class + func + mod + data + const + meth + attr + exc + obj +rst-directives = + note + warning + versionadded + versionchanged + deprecated + seealso +per-file-ignores = + tests/*:D diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..592b5b2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: monthly + - package-ecosystem: pip + directory: "/.github/workflows" + schedule: + interval: monthly + - package-ecosystem: pip + directory: "/docs" + schedule: + interval: monthly + - package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + versioning-strategy: lockfile-only + allow: + - dependency-type: "all" diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..f7f83aa --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,66 @@ +--- +# Labels names are important as they are used by Release Drafter to decide +# regarding where to record them in changelog or if to skip them. +# +# The repository labels will be automatically configured using this file and +# the GitHub Action https://github.com/marketplace/actions/github-labeler. +- name: breaking + description: Breaking Changes + color: bfd4f2 +- name: bug + description: Something isn't working + color: d73a4a +- name: build + description: Build System and Dependencies + color: bfdadc +- name: ci + description: Continuous Integration + color: 4a97d6 +- name: dependencies + description: Pull requests that update a dependency file + color: 0366d6 +- name: documentation + description: Improvements or additions to documentation + color: 0075ca +- name: duplicate + description: This issue or pull request already exists + color: cfd3d7 +- name: enhancement + description: New feature or request + color: a2eeef +- name: github_actions + description: Pull requests that update Github_actions code + color: "000000" +- name: good first issue + description: Good for newcomers + color: 7057ff +- name: help wanted + description: Extra attention is needed + color: 008672 +- name: invalid + description: This doesn't seem right + color: e4e669 +- name: performance + description: Performance + color: "016175" +- name: python + description: Pull requests that update Python code + color: 2b67c6 +- name: question + description: Further information is requested + color: d876e3 +- name: refactoring + description: Refactoring + color: ef67c4 +- name: removal + description: Removals and Deprecations + color: 9ae7ea +- name: style + description: Style + color: c120e5 +- name: testing + description: Testing + color: b1fc6f +- name: wontfix + description: This will not be worked on + color: ffffff diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..6a94a5b --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,66 @@ +# See https://github.com/marketplace/actions/release-drafter for configuration +categories: + - title: ":boom: Breaking Changes" + label: "breaking" + - title: ":rocket: Features" + label: "enhancement" + - title: ":fire: Removals and Deprecations" + label: "removal" + - title: ":beetle: Fixes" + label: "bug" + - title: ":racehorse: Performance" + label: "performance" + - title: ":rotating_light: Testing" + label: "testing" + - title: ":construction_worker: Continuous Integration" + label: "ci" + - title: ":books: Documentation" + label: "documentation" + - title: ":hammer: Refactoring" + label: "refactoring" + - title: ":lipstick: Style" + label: "style" + - title: ":package: Dependencies" + labels: + - "dependencies" + - "build" +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +autolabeler: + - label: 'documentation' + files: + - '*.md' + branch: + - '/.*docs{0,1}.*/' + - label: 'bug' + branch: + - '/fix.*/' + title: + - '/fix/i' + - label: 'enhancement' + branch: + - '/feature.*|add-.+/' + title: + - '/feat:.+|feature:.+/i' + - label: "removal" + title: + - "/remove .*/i" + - label: "performance" + title: + - "/.* performance .*/i" + - label: "ci" + files: + - '.github/*' + - '.pre-commit-config.yaml' + - '.coveragrc' + - label: "style" + files: + - ".flake8" + - label: "refactoring" + title: + - "/.* refactor.*/i" + +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/auto-merge-deps.yml b/.github/workflows/auto-merge-deps.yml new file mode 100644 index 0000000..7dd998e --- /dev/null +++ b/.github/workflows/auto-merge-deps.yml @@ -0,0 +1,14 @@ +name: auto-merge + +on: + pull_request: + +jobs: + auto-merge: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: ahmadnassri/action-dependabot-auto-merge@v2 + with: + target: minor + github-token: ${{ secrets.AUTO_MERGE }} diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml new file mode 100644 index 0000000..b83e789 --- /dev/null +++ b/.github/workflows/check-build.yml @@ -0,0 +1,27 @@ +name: Check Distribution Build + +on: push + +jobs: + check-build: + name: Twine Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Build Tools + run: pip install build twine + + - name: Build a binary wheel + run: | + python -m build . + + - name: Check Distribution + run: | + twine check dist/* diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..38e2023 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,18 @@ +name: Labeler + +on: + push: + branches: + - main + +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - name: Check out the repository + uses: actions/checkout@v3 + + - name: Run Labeler + uses: crazy-max/ghaction-github-labeler@v4.1.0 + with: + skip-delete: true diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml new file mode 100644 index 0000000..714ab65 --- /dev/null +++ b/.github/workflows/release-draft.yml @@ -0,0 +1,21 @@ +name: Update Draft Release + +on: push + +jobs: + draft-release: + name: Update Draft Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Publish the release notes + uses: release-drafter/release-drafter@v5.24.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/testsuite.yaml b/.github/workflows/testsuite.yaml new file mode 100644 index 0000000..e32492d --- /dev/null +++ b/.github/workflows/testsuite.yaml @@ -0,0 +1,41 @@ +name: Test Suite +on: [push, pull_request] + + +jobs: + tests: + name: Test Suite + runs-on: ${{ matrix.os }} + # defaults: + # run: + # shell: bash -l {0} + env: + ENV_NAME: testing + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.8, 3.9, "3.10", "3.11"] + steps: + - uses: actions/checkout@master + with: + fetch-depth: 1 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Package and Test Deps + run: | + echo $(which python) + python -m pip install .[tests] + + - name: Run Tests + run: | + python -m pytest --cov-config=.coveragerc --cov-report xml:./coverage.xml + + - uses: codecov/codecov-action@v3 + if: success() + with: + file: ./coverage.xml #optional diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..24f8a40 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,59 @@ +exclude: '^docs/conf.py' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: check-added-large-files + - id: check-ast + - id: check-json + - id: check-merge-conflict + - id: check-xml + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: mixed-line-ending + args: ['--fix=no'] + +- repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-comprehensions + - flake8-logging-format + - flake8-builtins + - flake8-eradicate + - pep8-naming + - flake8-pytest + - flake8-docstrings + - flake8-rst-docstrings + - flake8-rst + - flake8-copyright + - flake8-markdown + - flake8-bugbear + - flake8-comprehensions + - flake8-print + + +- repo: https://github.com/psf/black + rev: 23.7.0 + hooks: + - id: black + +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + +- repo: https://github.com/asottile/pyupgrade + rev: v3.9.0 + hooks: + - id: pyupgrade + args: [--py38-plus] diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ceb8132..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -dist: xenial - -python: - - 3.5 - - 3.6 - - 3.7 - -install: - - pip install tox-travis - - pip install . - -script: - - tox \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 622b4e4..e0f17aa 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -56,9 +56,9 @@ If you are proposing a feature: Get Started! ------------ -Ready to contribute? Here's how to set up `summarize` for local development. +Ready to contribute? Here's how to set up ``summarize`` for local development. -1. Fork the `beet-summarize` repo on GitHub. +1. Fork the ``beet-summarize`` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/beet-summarize.git diff --git a/README.md b/README.md index f99331d..bd47f6b 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ $ beet summarize genre | count ---------------------- | ----- -Rock | 340 -Classical | 268 -Folk | 248 -Pop | 248 +Rock | 340 +Classical | 268 +Folk | 248 +Pop | 248 ``` ``beet-summarize`` is a plugin for the ``beets`` music organisation library, @@ -52,21 +52,21 @@ $ beet summarize -g year -R year | count ---- | ----- -1981 | 1 -1991 | 4 -1985 | 9 -1982 | 10 -1990 | 11 +1981 | 1 +1991 | 4 +1985 | 9 +1982 | 10 +1990 | 11 ``` -Perhaps most importantly, you can specify aggregate statistics to report via -``-s`` or ``--stats``. Each statistic is a valid field with optional pre-pending -modifiers. Modifiers include an aggregation function (options are ``MIN``, -``MAX``, ``SUM``, ``COUNT``, ``AVG``, ``RANGE``), whether to only include -``UNIQUE`` entries, and converters for when the field is of str type -(options are ``LEN`` and ``WORDS``). The default statistic is ``count``. -You can give multiple statistics by enclosing them in quotes. The order of the results will -be based on the first given statistic. The format for each statistic is +Perhaps most importantly, you can specify aggregate statistics to report via +``-s`` or ``--stats``. Each statistic is a valid field with optional pre-pending +modifiers. Modifiers include an aggregation function (options are ``MIN``, +``MAX``, ``SUM``, ``COUNT``, ``AVG``, ``RANGE``), whether to only include +``UNIQUE`` entries, and converters for when the field is of str type +(options are ``LEN`` and ``WORDS``). The default statistic is ``count``. +You can give multiple statistics by enclosing them in quotes. The order of the results will +be based on the first given statistic. The format for each statistic is ``aggregator<:modifier>|field``, except for ``count`` which is special and does not require a field. @@ -77,14 +77,14 @@ $ beet summarize -g year -s "count avg|bitrate avg:words|lyrics count:unique|art year | count | avg|bitrate | avg:words|lyrics | count:unique|artist ---- | ----- | ----------------- | ------------------ | ------------------- -2006 | 317 | 648899.5741324921 | 273.51419558359623 | 41 -2009 | 244 | 709426.0778688524 | 660.7786885245902 | 17 -2005 | 241 | 754819.5145228215 | 681.6099585062241 | 24 -2010 | 203 | 747686.5615763547 | 537.1133004926108 | 51 +2006 | 317 | 648899.5741324921 | 273.51419558359623 | 41 +2009 | 244 | 709426.0778688524 | 660.7786885245902 | 17 +2005 | 241 | 754819.5145228215 | 681.6099585062241 | 24 +2010 | 203 | 747686.5615763547 | 537.1133004926108 | 51 ``` -Here the first statistic is just the number of tracks in the library for each -given year (and this sorts the table). The second column is the average +Here the first statistic is just the number of tracks in the library for each +given year (and this sorts the table). The second column is the average bitrate of tracks per year. The third column is the average number of words in the lyrics of each track per year. The final column is the number of unique artists on tracks per year. @@ -95,18 +95,18 @@ For string-valued fields (eg. artist, lyrics), if the statistic is not ``count`` some way to convert the field string to a numerical value is required. A number of ways could be thought of, but currently ``summarize`` only supports the number of words or number of characters, specified by the modifiers ``words`` -and ``len`` respectively. The latter is default. ``words`` are defined by +and ``len`` respectively. The latter is default. ``words`` are defined by characters separated by spaces. ### ``unique`` modifier The ``unique`` modifier is always applied directly on the field. This is usually what is wanted: eg. ``count:unique|artist`` produces the number of *different* -artists. However, it can be a little confusing in scenarios such as +artists. However, it can be a little confusing in scenarios such as ``sum:unique:words|lyrics``. This does *not* count the total number of unique words in all the lyrics for tracks in the given category. Instead, it applies the ``unique`` modifier to the field as a whole. So, it is the sum of the total -number of words for each track that has unique lyrics. +number of words for each track that has unique lyrics. ## Ideas for improvement @@ -118,7 +118,4 @@ number of words for each track that has unique lyrics. of unique words in all lyrics for a given year is possible. - Multi-level categories, i.e. being able to categorize by genre, then by year within the genre, and provide statistics at each level. -- Ability to pass an ``-a`` flag to do stats at the album-level. - - - +- Ability to pass an ``-a`` flag to do stats at the album-level. diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py index 299975c..de7876e 100644 --- a/beetsplug/__init__.py +++ b/beetsplug/__init__.py @@ -1,5 +1,5 @@ +"""Beets plug-in for generating statistics about your music library.""" from pkgutil import extend_path -__version__ = '0.2.0' -__all__ = ['parse_stat', 'show_summary', 'summarize'] +__version__ = "0.2.0" __path__ = extend_path(__path__, __name__) diff --git a/beetsplug/summarize.py b/beetsplug/summarize.py index 23657b9..4577f2c 100644 --- a/beetsplug/summarize.py +++ b/beetsplug/summarize.py @@ -1,3 +1,5 @@ +"""Summarize library statistics.""" + from collections import OrderedDict from beets.plugins import BeetsPlugin @@ -6,30 +8,34 @@ summarize_command = Subcommand("summarize", help="summarize library statistics") summarize_command.parser.add_option( - u"-g", u"--group-by", type="string", help=u"field to group by", default="genre" + "-g", "--group-by", type="string", help="field to group by", default="genre" ) summarize_command.parser.add_option( - u"-s", u"--stats", type="string", help=u"stats to display", default="count" + "-s", "--stats", type="string", help="stats to display", default="count" ) summarize_command.parser.add_option( - u"-R", u"--not-reverse", action="store_true", help="whether to not reverse the sort" + "-R", "--not-reverse", action="store_true", help="whether to not reverse the sort" ) def parse_stat(stat): - """Parse a cmdline stat string - - :param stat: string specifying the statistic to obtain. Format is - "aggregator<:modifier>|field". Available aggregators are {min, max, - count, sum, avg, range}. Available modifiers are {unique, len, words}, - where the final two are only available for string fields. Available - `fields` are any beets field. - - :return: dict specifying the stat. Keys are 'field' (str field), - 'aggregator' (str aggregator), 'str_converter' (either 'len' or 'words' - or None), and 'unique' (bool). + """Parse a cmdline stat string. + + Parameters + ---------- + stat + string specifying the statistic to obtain. Format is + "aggregator<:modifier>|field". Available aggregators are {min, max, count, sum, + avg, range}. Available modifiers are {unique, len, words}, where the final two + are only available for string fields. Available `fields` are any beets field. + + Returns + ------- + dict + The statistics. Keys are 'field' (str field), 'aggregator' (str aggregator), + 'str_converter' (either 'len' or 'words' or None), and 'unique' (bool). """ aggregators = ["min", "max", "count", "sum", "avg", "range"] str_converters = ["len", "words"] @@ -71,7 +77,7 @@ def parse_stat(stat): "You have specified more than one str conversion: " "{}".format(stat) ) else: - if "str_converter" in this and this['str_converter']: + if "str_converter" in this and this["str_converter"]: this["str_converter"] = this["str_converter"][0] else: this["str_converter"] = None @@ -82,27 +88,32 @@ def parse_stat(stat): return this -def parse_stats(stats): - """Parse a cmdline stats string +def parse_stats(stats: str) -> dict[str, dict]: + """Parse a cmdline stats string. - Args: - stats (str) : string with stats separated by spaces. For format of - stats, see :func:`parse_stat`. + Parameters + ---------- + stats + string with stats separated by spaces. For format of stats, see + :func:`parse_stat`. - Returns: - OrderedDict : keys are each full stat string in `stats`. Values are - dictionaries from `parse_stat` for each stat. - str : the first stat. + Returns + ------- + OrderedDict + keys are each full stat string in `stats`. Values are + dictionaries from `parse_stat` for each stat. + str + the first stat. """ stats = stats.split(" ") - out_dct = OrderedDict([(stat, parse_stat(stat)) for stat in stats]) - return out_dct + return OrderedDict([(stat, parse_stat(stat)) for stat in stats]) def set_str_converter(stat, stat_type): - """Set str_converter field for a stat dict if the field type is str - and the converter does not yet exist""" + """Set str_converter field for a stat dict. + Only applies if the field type is str and the converter does not yet exist. + """ # For strings arguments, require a way to turn # the string into a numerical value. By default, # use the length of the string. @@ -111,6 +122,7 @@ def set_str_converter(stat, stat_type): def group_by(category, items): + """Group a list of items by a category.""" out = {} for item in items: cat = getattr(item, category) @@ -124,10 +136,8 @@ def group_by(category, items): def get_items_stat(items, stat): - if stat["unique"]: - collection = set() - else: - collection = [] + """Get a statistic for a list of items.""" + collection = set() if stat["unique"] else [] # Collect all the stats for item in items: @@ -166,37 +176,33 @@ def get_items_stat(items, stat): def print_dct_as_table(keys, dcts, cat_name=None, col_formats=None): - """ Pretty print a list of dictionaries (myDict) as a dynamically sized table. - If column names (colList) aren't specified, they will show in random order. + """Pretty print a list of dictionaries as a dynamically sized table. + If column names aren't specified, they will show in random order. """ columns = list(dcts[0].keys()) - if cat_name: - table = [[cat_name] + columns] # 1st row = header - else: - table = [[""] + columns] + table = [[cat_name] + columns] if cat_name else [[""] + columns] for key, item in zip(keys, dcts): - table.append( - ["{}".format(key)] - + [ - "{val:{fmt}}".format( - val=item[col], fmt=col_formats[col] if col_formats else "" - ) - for col in columns - ] - ) + content = [ + "{val:{fmt}}".format( + val=item[col], fmt=col_formats[col] if col_formats else "" + ) + for col in columns + ] + table.append([key] + content) col_size = [max(map(len, col)) for col in zip(*table)] - formatStr = " | ".join(["{{:<{}}}".format(i) for i in col_size]) + fmt_str = " | ".join([f"{{:<{i}}}" for i in col_size]) table.insert(1, ["-" * i for i in col_size]) # Seperating line for item in table: - print(formatStr.format(*item)) + print(fmt_str.format(*item)) def print_results(res, cat_name, sort_stat, reverse): + """Print the results of a summary.""" keys = sorted(res.keys(), key=lambda x: res[x][sort_stat], reverse=reverse) dcts = [res[key] for key in keys] @@ -204,9 +210,9 @@ def print_results(res, cat_name, sort_stat, reverse): def show_summary(lib, query, category, stats, reverse): - """ + """Show a summary of the statistics.""" # TODO: albums? - """ + items = lib.items(query) stats = parse_stats(stats) sort_stat = list(stats.keys())[0] @@ -216,17 +222,15 @@ def show_summary(lib, query, category, stats, reverse): groups = group_by(category, items) - stat_dct = {} - for g, items in groups.items(): - stat_dct[g] = {} - - for nm, stat in stats.items(): - stat_dct[g][nm] = get_items_stat(items, stat) - + stat_dct = { + g: {nm: get_items_stat(items, stat) for nm, stat in stats.items()} + for g, items in groups.items() + } print_results(stat_dct, category, sort_stat, reverse) def summarize(lib, opts, args): + """Summarize the library by a given field.""" show_summary(lib, decargs(args), opts.group_by, opts.stats, not opts.not_reverse) @@ -234,5 +238,8 @@ def summarize(lib, opts, args): class SuperPlug(BeetsPlugin): + """Subclass of the BeetsPlugin to create the command.""" + def commands(self): + """Add the summarize command.""" return [summarize_command] diff --git a/pyproject.toml b/pyproject.toml index d200087..830e4e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,9 +42,14 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] dynamic = ["version"] -urls = [ - "https://github.com/steven-murray/beet-summarize", -] + +[project.urls] +Repository = "https://github.com/steven-murray/beet-summarize" +Changelog = "https://github.com/steven-murray/beet-summarize/releases" [project.optional-dependencies] tests = ["pytest"] +dev = [ + "pre-commit", + "summarize[tests]", +] diff --git a/tests/test_summarize.py b/tests/test_summarize.py index e09004c..0216aae 100644 --- a/tests/test_summarize.py +++ b/tests/test_summarize.py @@ -1,7 +1,7 @@ -import pytest -import summarize as sm import sys +from beetsplug import summarize as sm + def test_parse_stat(): print(sys.path) From 825c7f4b653ffe83b68800bc6d76f4b2b2980b43 Mon Sep 17 00:00:00 2001 From: Steven Murray Date: Tue, 11 Jul 2023 12:52:18 -0700 Subject: [PATCH 3/4] build: add beets as a dep --- pyproject.toml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 830e4e2..37fa234 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] +dependencies = [ + 'beets>=1.5.0', +] dynamic = ["version"] [project.urls] @@ -48,7 +51,11 @@ Repository = "https://github.com/steven-murray/beet-summarize" Changelog = "https://github.com/steven-murray/beet-summarize/releases" [project.optional-dependencies] -tests = ["pytest"] +tests = [ + "pytest", + "pytest-cov" +] + dev = [ "pre-commit", "summarize[tests]", From 22d4cb8ee3d6c4303e29a9bbf314de8e7e357383 Mon Sep 17 00:00:00 2001 From: Steven Murray Date: Tue, 11 Jul 2023 12:53:49 -0700 Subject: [PATCH 4/4] fix: add future annotations --- beetsplug/summarize.py | 1 + 1 file changed, 1 insertion(+) diff --git a/beetsplug/summarize.py b/beetsplug/summarize.py index 4577f2c..81d9a5f 100644 --- a/beetsplug/summarize.py +++ b/beetsplug/summarize.py @@ -1,4 +1,5 @@ """Summarize library statistics.""" +from __future__ import annotations from collections import OrderedDict