Skip to content

Commit

Permalink
Upgrade to Python 3.8 / drop Python 3.7 (#5283)
Browse files Browse the repository at this point in the history
## Description

Fixes #5222. 

Drop Python 3.7. `pyupgrade` is responsible for most of the changes in
the code. I undid some of the bits it attempted to update that aren't
strictly necessary:

1. Converting `List/Dict/Tuple` -> `list/dict/tuple` in modules that
have `from __future__ import annotations` import. This should be done in
a separate PR, and for all modules
2. Converting some `.format(` calls to f-strings. It didn't do it
consistently, and it should also be done in a separate PR, I believe.

Python upgrade unblocks several other PRs, for example #5266 and #5248.
  • Loading branch information
snejus authored Jun 5, 2024
2 parents d4ecd5f + 65bfbda commit 1b59479
Show file tree
Hide file tree
Showing 16 changed files with 41 additions and 101 deletions.
18 changes: 5 additions & 13 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest, windows-latest]
python-version: ['3.7', '3.8', '3.9', '3.x']
python-version: ["3.8", "3.9", "3.x"]

env:
PY_COLORS: 1
Expand All @@ -25,15 +25,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

# tox fails on Windows if version > 3.8.3
- name: Install base dependencies - Windows
if: matrix.platform == 'windows-latest'
run: |
python -m pip install --upgrade pip
python -m pip install tox==3.8.3 sphinx
- name: Install base dependencies - Ubuntu
if: matrix.platform != 'windows-latest'
- name: Install base dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox sphinx
Expand All @@ -50,7 +42,7 @@ jobs:
tox -e py-test
- name: Upload code coverage
if: matrix.python-version == '3.7' && matrix.platform == 'ubuntu-latest'
if: matrix.python-version == '3.8' && matrix.platform == 'ubuntu-latest'
run: |
pip install codecov || true
codecov || true
Expand All @@ -76,7 +68,7 @@ jobs:
- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: "3.x"

- name: Install base dependencies
run: |
Expand All @@ -98,7 +90,7 @@ jobs:
- name: Set up Python 3.x
uses: actions/setup-python@v2
with:
python-version: '3.x'
python-version: "3.x"

- name: Install base dependencies
run: |
Expand Down
12 changes: 1 addition & 11 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ Running the Tests

To run the tests for multiple Python versions, compile the docs, and
check style, use `tox`_. Just type ``tox`` or use something like
``tox -e py27`` to test a specific configuration. You can use the
``tox -e py38`` to test a specific configuration. You can use the
``--parallel`` flag to make this go faster.

You can disable a hand-selected set of "slow" tests by setting the
Expand All @@ -294,13 +294,6 @@ Other ways to run the tests:
- ``python -m unittest discover -p 'test_*'`` (ditto)
- `pytest`_

You can also see the latest test results on `Linux`_ and on `Windows`_.

Note, if you are on Windows and are seeing errors running tox, it may be related to `this issue`_,
in which case you may have to install tox v3.8.3 e.g. ``python -m pip install tox==3.8.3``

.. _this issue: https://github.com/tox-dev/tox/issues/1550

Coverage
^^^^^^^^

Expand Down Expand Up @@ -360,9 +353,6 @@ others. See `unittest.mock`_ for more info.
.. _pytest-random: https://github.com/klrmn/pytest-random
.. _tox: https://tox.readthedocs.io/en/latest/
.. _pytest: https://docs.pytest.org/en/stable/
.. _Linux: https://github.com/beetbox/beets/actions
.. _Windows: https://ci.appveyor.com/project/beetbox/beets/
.. _`https://github.com/beetbox/beets/blob/master/setup.py#L99`: https://github.com/beetbox/beets/blob/master/setup.py#L99
.. _test: https://github.com/beetbox/beets/tree/master/test
.. _`https://github.com/beetbox/beets/blob/master/test/test_template.py#L224`: https://github.com/beetbox/beets/blob/master/test/test_template.py#L224
.. _unittest: https://docs.python.org/3/library/unittest.html
Expand Down
2 changes: 1 addition & 1 deletion beets/autotag/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo:
# Media (format).
if release["medium-list"]:
# If all media are the same, use that medium name
if len(set([m.get("format") for m in release["medium-list"]])) == 1:
if len({m.get("format") for m in release["medium-list"]}) == 1:
info.media = release["medium-list"][0].get("format")
# Otherwise, let's just call it "Media"
else:
Expand Down
24 changes: 7 additions & 17 deletions beets/dbcore/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,22 @@

"""Representation of type information for DBCore model fields.
"""
import sys
import typing
from abc import ABC
from typing import TYPE_CHECKING, Any, Generic, List, TypeVar, Union, cast
from typing import Any, Generic, List, TypeVar, Union, cast

from beets.util import str2bool

from .query import BooleanQuery, FieldQuery, NumericQuery, SubstringQuery

# Abstract base.

class ModelType(typing.Protocol):
"""Protocol that specifies the required constructor for model types,
i.e. a function that takes any argument and attempts to parse it to the
given type.
"""

# FIXME: unconditionally define the Protocol once we drop Python 3.7
if TYPE_CHECKING and sys.version_info >= (3, 8):

class ModelType(typing.Protocol):
"""Protocol that specifies the required constructor for model types,
i.e. a function that takes any argument and attempts to parse it to the
given type.
"""

def __init__(self, value: Any = None): ...

else:
# No structural subtyping in Python < 3.8...
ModelType = Any
def __init__(self, value: Any = None): ...


# Generic type variables, used for the value type T and null type N (if
Expand Down
6 changes: 3 additions & 3 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import sys
import time
import unicodedata
from functools import cached_property

from mediafile import MediaFile, UnreadableFileError

Expand All @@ -31,7 +32,6 @@
from beets.util import (
MoveOperation,
bytestring_path,
lazy_property,
normpath,
samefile,
syspath,
Expand Down Expand Up @@ -436,11 +436,11 @@ def __init__(self, item, included_keys=ALL_KEYS, for_path=False):
self.model_keys = included_keys
self.item = item

@lazy_property
@cached_property
def all_keys(self):
return set(self.model_keys).union(self.album_keys)

@lazy_property
@cached_property
def album_keys(self):
album_keys = []
if self.album:
Expand Down
8 changes: 1 addition & 7 deletions beets/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@


import logging
import sys
import threading
from copy import copy

Expand Down Expand Up @@ -84,12 +83,7 @@ def _log(
m = self._LogMessage(msg, args, kwargs)

stacklevel = kwargs.pop("stacklevel", 1)
if sys.version_info >= (3, 8):
stacklevel = {"stacklevel": stacklevel}
else:
# Simply ignore this when not supported by current Python version.
# Can be dropped when we remove support for Python 3.7.
stacklevel = {}
stacklevel = {"stacklevel": stacklevel}

return super()._log(
level,
Expand Down
10 changes: 5 additions & 5 deletions beets/ui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _paths_from_logfile(path):
"""Parse the logfile and yield skipped paths to pass to the `import`
command.
"""
with open(path, mode="r", encoding="utf-8") as fp:
with open(path, encoding="utf-8") as fp:
for i, line in enumerate(fp, start=1):
verb, sep, paths = line.rstrip("\n").partition(" ")
if not sep:
Expand All @@ -117,7 +117,7 @@ def _parse_logfiles(logfiles):
util.displayable_path(logfile), str(err)
)
) from err
except IOError as err:
except OSError as err:
raise ui.UserError(
"unreadable logfile {}: {}".format(
util.displayable_path(logfile), str(err)
Expand Down Expand Up @@ -300,7 +300,7 @@ def penalty_string(distance, limit=None):
return ui.colorize("changed", penalty_string)


class ChangeRepresentation(object):
class ChangeRepresentation:
"""Keeps track of all information needed to generate a (colored) text
representation of the changes that will be made if an album or singleton's
tags are changed according to `match`, which must be an AlbumMatch or
Expand Down Expand Up @@ -654,7 +654,7 @@ class AlbumChange(ChangeRepresentation):
"""Album change representation, setting cur_album"""

def __init__(self, cur_artist, cur_album, match):
super(AlbumChange, self).__init__()
super().__init__()
self.cur_artist = cur_artist
self.cur_album = cur_album
self.match = match
Expand Down Expand Up @@ -722,7 +722,7 @@ class TrackChange(ChangeRepresentation):
"""Track change representation, comparing item with match."""

def __init__(self, cur_artist, cur_title, match):
super(TrackChange, self).__init__()
super().__init__()
self.cur_artist = cur_artist
self.cur_title = cur_title
self.match = match
Expand Down
24 changes: 0 additions & 24 deletions beets/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import errno
import fnmatch
import functools
import os
import platform
import re
Expand Down Expand Up @@ -1106,26 +1105,3 @@ def par_map(transform: Callable, items: Iterable):
pool.map(transform, items)
pool.close()
pool.join()


def lazy_property(func: Callable) -> Callable:
"""A decorator that creates a lazily evaluated property. On first access,
the property is assigned the return value of `func`. This first value is
stored, so that future accesses do not have to evaluate `func` again.
This behaviour is useful when `func` is expensive to evaluate, and it is
not certain that the result will be needed.
"""
field_name = "_" + func.__name__

@property
@functools.wraps(func)
def wrapper(self):
if hasattr(self, field_name):
return getattr(self, field_name)

value = func(self)
setattr(self, field_name, value)
return value

return wrapper
2 changes: 1 addition & 1 deletion beets/util/artresizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def deinterlace(self, path_in, path_out=None):
im = Image.open(syspath(path_in))
im.save(py3_path(path_out), progressive=False)
return path_out
except IOError:
except OSError:
# FIXME: Should probably issue a warning?
return path_in

Expand Down
9 changes: 2 additions & 7 deletions beets/util/functemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import dis
import functools
import re
import sys
import types

SYMBOL_DELIM = "$"
Expand Down Expand Up @@ -97,8 +96,7 @@ def compile_func(arg_names, statements, name="_the_func", debug=False):
"kw_defaults": [],
"defaults": [ex_literal(None) for _ in arg_names],
}
if "posonlyargs" in ast.arguments._fields: # Added in Python 3.8.
args_fields["posonlyargs"] = []
args_fields["posonlyargs"] = []
args = ast.arguments(**args_fields)

func_def = ast.FunctionDef(
Expand All @@ -110,10 +108,7 @@ def compile_func(arg_names, statements, name="_the_func", debug=False):

# The ast.Module signature changed in 3.8 to accept a list of types to
# ignore.
if sys.version_info >= (3, 8):
mod = ast.Module([func_def], [])
else:
mod = ast.Module([func_def])
mod = ast.Module([func_def], [])

ast.fix_missing_locations(mod)

Expand Down
2 changes: 1 addition & 1 deletion beetsplug/discogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def check_discogs_client(self):
]
if True not in gt_min:
self._log.warning(
("python3-discogs-client version should be " ">= 2.3.15")
"python3-discogs-client version should be >= 2.3.15"
)

def setup(self, session=None):
Expand Down
2 changes: 1 addition & 1 deletion beetsplug/substitute.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self):
Get the configuration, register template function and create list of
substitute rules.
"""
super(Substitute, self).__init__()
super().__init__()
self.substitute_rules = []
self.template_funcs["substitute"] = self.tmpl_substitute

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ Bug fixes:

* Improved naming of temporary files by separating the random part with the file extension.

For packagers:

* The minimum Python version is now 3.8.

2.0.0 (May 30, 2024)
--------------------

Expand Down
12 changes: 6 additions & 6 deletions docs/dev/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ an example::

class SomePlugin(BeetsPlugin):
def __init__(self):
super(SomePlugin, self).__init__()
super().__init__()
self.register_listener('pluginload', loaded)

Note that if you want to access an attribute of your plugin (e.g. ``config`` or
Expand All @@ -125,7 +125,7 @@ registration process in this case::

class SomePlugin(BeetsPlugin):
def __init__(self):
super(SomePlugin, self).__init__()
super().__init__()
self.register_listener('pluginload', self.loaded)

def loaded(self):
Expand Down Expand Up @@ -354,7 +354,7 @@ Here's an example::

class MyPlugin(BeetsPlugin):
def __init__(self):
super(MyPlugin, self).__init__()
super().__init__()
self.template_funcs['initial'] = _tmpl_initial

def _tmpl_initial(text):
Expand All @@ -374,7 +374,7 @@ Here's an example that adds a ``$disc_and_track`` field::

class MyPlugin(BeetsPlugin):
def __init__(self):
super(MyPlugin, self).__init__()
super().__init__()
self.template_fields['disc_and_track'] = _tmpl_disc_and_track

def _tmpl_disc_and_track(item):
Expand Down Expand Up @@ -452,7 +452,7 @@ to register it::
from beets.plugins import BeetsPlugin
class ExamplePlugin(BeetsPlugin):
def __init__(self):
super(ExamplePlugin, self).__init__()
super().__init__()
self.import_stages = [self.stage]
def stage(self, session, task):
print('Importing something!')
Expand Down Expand Up @@ -596,7 +596,7 @@ plugin shall expose to the user::

class ExamplePlugin(BeetsPlugin):
def __init__(self):
super(ExamplePlugin, self).__init__()
super().__init__()
self.register_listener('before_choose_candidate',
self.before_choose_candidate_event)

Expand Down
Loading

0 comments on commit 1b59479

Please sign in to comment.