Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace most str.format() uses with f-strings #5337

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion beets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def read(self, user=True, defaults=True):
except confuse.NotFoundError:
pass
except confuse.ConfigReadError as err:
stderr.write("configuration `import` failed: {}".format(err.reason))
stderr.write(f"configuration `import` failed: {err.reason}")


config = IncludeLazyConfig("beets", __name__)
23 changes: 13 additions & 10 deletions beets/autotag/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,6 @@ def copy(self) -> TrackInfo:
# Candidate distance scoring.

# Parameters for string distance function.
# Words that can be moved to the end of a string using a comma.
SD_END_WORDS = ["the", "a", "an"]
# Reduced weights for certain portions of the string.
SD_PATTERNS = [
(r"^the ", 0.1),
Expand Down Expand Up @@ -311,17 +309,24 @@ def string_dist(str1: Optional[str], str2: Optional[str]) -> float:
if str1 is None or str2 is None:
return 1.0

# Make all following comparison case-insensitive.
str1 = str1.lower()
str2 = str2.lower()

# Don't penalize strings that move certain words to the end. For
# example, "the something" should be considered equal to
# "something, the".
for word in SD_END_WORDS:
if str1.endswith(", %s" % word):
str1 = "{} {}".format(word, str1[: -len(word) - 2])
if str2.endswith(", %s" % word):
str2 = "{} {}".format(word, str2[: -len(word) - 2])
def switch_article(string: str) -> str:
if ", " not in string:
return string
[title, article] = string.rsplit(", ", maxsplit=1)
if article in ["the", "a", "an"]:
return f"{article} {title}"
else:
return string

str1 = switch_article(str1)
str2 = switch_article(str2)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither the old option nor your version is incredibly readable. However I think I prefer the old version, because "endswith" is more readable. Could you maybe change var m to something more descriptive? And maybe change replacer to something that sounds more like a function? Like do_replace?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I think a more intuitive implementation would be based on str.rsplit() or str.rpartition() using , as a delimiter, and checking that the final component is one of the three words. If that doesn't sound good, I can improve what I have here: m -> match_info and replacer -> move_article_to_front?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with @RollingStar actually, I think your regex is clearer. Maybe I'm just more familiar/confident with regex? One thing I would note is that it might be better, if we stay with this approach, to make the regex case-insensitive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've rewritten this as a much more explicit and intuitive function which uses str.rsplit to separate a title from an article. @RollingStar, I hope this makes it more readable! Also, @Serene-Arc, the inputs are lower-cased here, so case sensitivity is not an issue.

# Perform a couple of basic normalizing substitutions.
for pat, repl in SD_REPLACE:
Expand Down Expand Up @@ -469,9 +474,7 @@ def keys(self) -> List[str]:
def update(self, dist: "Distance"):
"""Adds all the distance penalties from `dist`."""
if not isinstance(dist, Distance):
raise ValueError(
"`dist` must be a Distance object, not {}".format(type(dist))
)
raise ValueError(f"`dist` must be a Distance object, not {dist}")
for key, penalties in dist._penalties.items():
self._penalties.setdefault(key, []).extend(penalties)

Expand Down
4 changes: 2 additions & 2 deletions beets/autotag/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def __init__(self, reason, verb, query, tb=None):
super().__init__(reason, verb, tb)

def get_message(self):
return "{} in {} with query {}".format(
self._reasonstr(), self.verb, repr(self.query)
return (
f"{self._reasonstr()} in {self.verb} with query {repr(self.query)}"
)


Expand Down
104 changes: 55 additions & 49 deletions beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,9 @@ def _awaken(
return obj

def __repr__(self) -> str:
return "{}({})".format(
type(self).__name__,
", ".join(f"{k}={v!r}" for k, v in dict(self).items()),
)
name = type(self).__name__
fields = ", ".join(f"{k}={repr(v)}" for k, v in dict(self).items())
return f"{name}({fields})"

def clear_dirty(self):
"""Mark all fields as *clean* (i.e., not needing to be stored to
Expand All @@ -414,10 +413,11 @@ def _check_db(self, need_id: bool = True) -> Database:
has a reference to a database (`_db`) and an id. A ValueError
exception is raised otherwise.
"""
name = type(self).__name__
if not self._db:
raise ValueError("{} has no database".format(type(self).__name__))
raise ValueError(f"{name} has no database")
if need_id and not self.id:
raise ValueError("{} has no id".format(type(self).__name__))
raise ValueError(f"{name} has no id")

return self._db

Expand Down Expand Up @@ -558,12 +558,12 @@ def __iter__(self) -> Iterator[str]:

def __getattr__(self, key):
if key.startswith("_"):
raise AttributeError(f"model has no attribute {key!r}")
raise AttributeError(f"model has no attribute {repr(key)}")
else:
try:
return self[key]
except KeyError:
raise AttributeError(f"no such field {key!r}")
raise AttributeError(f"no such field {repr(key)}")

def __setattr__(self, key, value):
if key.startswith("_"):
Expand All @@ -580,46 +580,53 @@ def __delattr__(self, key):
# Database interaction (CRUD methods).

def store(self, fields: Optional[Iterable[str]] = None):
"""Save the object's metadata into the library database.
:param fields: the fields to be stored. If not specified, all fields
will be.
"""
if fields is None:
fields = self._fields
Save the object's metadata into the library database.

:param fields: the fields to be stored (default: all of them).
Only non-flexible fields (excluding `"id"`) can be specified.
"""

db = self._check_db()

# Build assignments for query.
assignments = []
subvars = []
for key in fields:
if key != "id" and key in self._dirty:
self._dirty.remove(key)
assignments.append(key + "=?")
value = self._type(key).to_sql(self[key])
subvars.append(value)
# Extract known sets of keys from the table.
known_fields = set(self._fields.keys())
known_flex_fields = set(self._values_flex.keys())

# Normalize the 'fields' parameter.
fields = set(fields or known_fields) - {"id"}
assert (
len(fields & known_flex_fields) == 0
), "`fields` cannot contain flexible fields"

# Compute how various fields were modified.
dirty_fields = fields & self._dirty
dirty_flex_fields = known_flex_fields & self._dirty
removed_flex_fields = self._dirty - fields - known_flex_fields

with db.transaction() as tx:
# Main table update.
if assignments:
query = "UPDATE {} SET {} WHERE id=?".format(
self._table, ",".join(assignments)
)
subvars.append(self.id)
tx.mutate(query, subvars)
# Update non-flexible fields.
if dirty_fields:
# NOTE: the order of iteration of 'dirty_fields' will not change
# between the two statements, since the set is not modified.
assignments = ",".join(f"{k}=?" for k in dirty_fields)
values = [self._type(k).to_sql(self[k]) for k in dirty_fields]
query = f"UPDATE {self._table} SET {assignments} WHERE id=?"
tx.mutate(query, [*values, self.id])

# Modified/added flexible attributes.
for key, value in self._values_flex.items():
if key in self._dirty:
self._dirty.remove(key)
tx.mutate(
"INSERT INTO {} "
"(entity_id, key, value) "
"VALUES (?, ?, ?);".format(self._flex_table),
(self.id, key, value),
)
# TODO: Use the underlying 'executemany()' function here.
for key in dirty_flex_fields:
tx.mutate(
f"INSERT INTO {self._flex_table} "
"(entity_id, key, value) "
"VALUES (?, ?, ?)",
(self.id, key, self._values_flex[key]),
)

# Deleted flexible attributes.
for key in self._dirty:
# TODO: Use the underlying 'executemany()' function here.
for key in removed_flex_fields:
tx.mutate(
f"DELETE FROM {self._flex_table} WHERE entity_id=? AND key=?",
(self.id, key),
Expand Down Expand Up @@ -1192,18 +1199,17 @@ def _make_table(self, table: str, fields: Mapping[str, types.Type]):
columns = []
for name, typ in fields.items():
columns.append(f"{name} {typ.sql}")
setup_sql = "CREATE TABLE {} ({});\n".format(
table, ", ".join(columns)
)
columns_def = ", ".join(columns)
setup_sql = f"CREATE TABLE {table} ({columns_def});\n"

else:
# Table exists does not match the field set.
setup_sql = ""
for name, typ in fields.items():
if name in current_fields:
continue
setup_sql += "ALTER TABLE {} ADD COLUMN {} {};\n".format(
table, name, typ.sql
setup_sql += (
f"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\n"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging here that we always want to be attentive when changing SQL commands.


with self.transaction() as tx:
Expand All @@ -1215,16 +1221,16 @@ def _make_attribute_table(self, flex_table: str):
"""
with self.transaction() as tx:
tx.script(
"""
CREATE TABLE IF NOT EXISTS {0} (
f"""
CREATE TABLE IF NOT EXISTS {flex_table} (
id INTEGER PRIMARY KEY,
entity_id INTEGER,
key TEXT,
value TEXT,
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
CREATE INDEX IF NOT EXISTS {0}_by_entity
ON {0} (entity_id);
""".format(flex_table)
CREATE INDEX IF NOT EXISTS {flex_table}_by_entity
ON {flex_table} (entity_id);
"""
)

# Querying.
Expand Down
27 changes: 13 additions & 14 deletions beets/dbcore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
self.fast = fast

def col_clause(self) -> Tuple[str, Sequence[SQLiteType]]:
# TODO: Avoid having to insert raw text into SQL clauses.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good edit

return self.field, ()

def clause(self) -> Tuple[Optional[str], Sequence[SQLiteType]]:
Expand All @@ -170,7 +171,7 @@

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, "
f"{self.__class__.__name__}({repr(self.field_name)}, {repr(self.pattern)}, "
f"fast={self.fast})"
)

Expand Down Expand Up @@ -209,7 +210,9 @@
return obj.get(self.field_name) is None

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.field_name!r}, {self.fast})"
return (
f"{self.__class__.__name__}({repr(self.field_name)}, {self.fast})"
)


class StringFieldQuery(FieldQuery[P]):
Expand Down Expand Up @@ -303,7 +306,7 @@
return unicodedata.normalize("NFC", s)

@classmethod
def string_match(cls, pattern: Pattern, value: str) -> bool:

Check failure on line 309 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "Pattern"
return pattern.search(cls._normalize(value)) is not None


Expand Down Expand Up @@ -465,7 +468,7 @@
"""Return a set with field names that this query operates on."""
return reduce(or_, (sq.field_names for sq in self.subqueries))

def __init__(self, subqueries: Sequence = ()):

Check failure on line 471 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "Sequence"
self.subqueries = subqueries

# Act like a sequence.
Expand All @@ -476,7 +479,7 @@
def __getitem__(self, key):
return self.subqueries[key]

def __iter__(self) -> Iterator:

Check failure on line 482 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "Iterator"
return iter(self.subqueries)

def __contains__(self, subq) -> bool:
Expand All @@ -502,7 +505,7 @@
return clause, subvals

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.subqueries!r})"
return f"{self.__class__.__name__}({repr(self.subqueries)})"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why !r got replaced by repr calls? As far as I'm aware !r is the preference here: https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snejus My reading of the PEP for f-strings led me to believe the opposite? They claim that they support the old calls, !r etc, to maintain compatibility.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout @Serene-Arc. What I gathered from that section is that they aren't required anymore since they can be replaced by the equivalent expressions, which was not supported previously.

Regarding the preference, from what I've seen in the wild Python community tends to use the native syntax supported by the f-string instead of an equivalent expression, if possible, for example:

  • Datetime
dt = datetime.now()
f"Datetime: {dt:%F %T}"
# vs
f'Datetime: {dt.strftime("%F %T")}'
  • Indentation
txt = "text"
f"{txt:>10}"
# vs
f'{" " * (10 - len(txt))}{txt}'

In a similar way, I prefer !r over repr call since there's no need to use an expression and it's more concise.

I asked perplexity to see what it thinks: https://www.perplexity.ai/search/should-r-or-repr-call-be-prefe-YGZ43OrrTGOYR2eNjl7QeQ


def __eq__(self, other) -> bool:
return super().__eq__(other) and self.subqueries == other.subqueries
Expand All @@ -525,7 +528,7 @@
"""Return a set with field names that this query operates on."""
return set(self.fields)

def __init__(self, pattern, fields, cls: Type[FieldQuery]):

Check failure on line 531 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "FieldQuery"
self.pattern = pattern
self.fields = fields
self.query_class = cls
Expand All @@ -547,7 +550,7 @@

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.pattern!r}, {self.fields!r}, "
f"{self.__class__.__name__}({repr(self.pattern)}, {repr(self.fields)}, "
f"{self.query_class.__name__})"
)

Expand All @@ -563,7 +566,7 @@
query is initialized.
"""

subqueries: MutableSequence

Check failure on line 569 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "MutableSequence"

def __setitem__(self, key, value):
self.subqueries[key] = value
Expand Down Expand Up @@ -618,7 +621,7 @@
return not self.subquery.match(obj)

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.subquery!r})"
return f"{self.__class__.__name__}({repr(self.subquery)})"

def __eq__(self, other) -> bool:
return super().__eq__(other) and self.subquery == other.subquery
Expand Down Expand Up @@ -791,9 +794,7 @@

def __init__(self, start: Optional[datetime], end: Optional[datetime]):
if start is not None and end is not None and not start < end:
raise ValueError(
"start date {} is not before end date {}".format(start, end)
)
raise ValueError(f"start date {start} is not before end date {end}")
self.start = start
self.end = end

Expand Down Expand Up @@ -841,20 +842,18 @@
date = datetime.fromtimestamp(timestamp)
return self.interval.contains(date)

_clause_tmpl = "{0} {1} ?"

def col_clause(self) -> Tuple[str, Sequence[SQLiteType]]:
clause_parts = []
subvals = []

# Convert the `datetime` objects to an integer number of seconds since
# the (local) Unix epoch using `datetime.timestamp()`.
if self.interval.start:
clause_parts.append(self._clause_tmpl.format(self.field, ">="))
clause_parts.append(f"{self.field} >= ?")
subvals.append(int(self.interval.start.timestamp()))

if self.interval.end:
clause_parts.append(self._clause_tmpl.format(self.field, "<"))
clause_parts.append(f"{self.field} < ?")
subvals.append(int(self.interval.end.timestamp()))

if clause_parts:
Expand Down Expand Up @@ -908,7 +907,7 @@
"""
return None

def sort(self, items: List) -> List:

Check failure on line 910 in beets/dbcore/query.py

View workflow job for this annotation

GitHub Actions / Check types with mypy

Missing type parameters for generic type "List"
"""Sort the list of objects and return a list."""
return sorted(items)

Expand Down Expand Up @@ -978,7 +977,7 @@
return items

def __repr__(self):
return f"{self.__class__.__name__}({self.sorts!r})"
return f"{self.__class__.__name__}({repr(self.sorts)})"

def __hash__(self):
return hash(tuple(self.sorts))
Expand Down Expand Up @@ -1018,7 +1017,7 @@
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}"
f"({self.field!r}, ascending={self.ascending!r})"
f"({repr(self.field)}, ascending={repr(self.ascending)})"
)

def __hash__(self) -> int:
Expand Down
4 changes: 1 addition & 3 deletions beets/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1582,9 +1582,7 @@ def resolve_duplicates(session, task):
if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG):
found_duplicates = task.find_duplicates(session.lib)
if found_duplicates:
log.debug(
"found duplicates: {}".format([o.id for o in found_duplicates])
)
log.debug("found duplicates: {0}", [o.id for o in found_duplicates])

# Get the default action to follow from config.
duplicate_action = config["import"]["duplicate_action"].as_choice(
Expand Down
15 changes: 6 additions & 9 deletions beets/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def col_clause(self):

def __repr__(self) -> str:
return (
f"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, "
f"{self.__class__.__name__}({repr(self.field)}, {repr(self.pattern)}, "
f"fast={self.fast}, case_sensitive={self.case_sensitive})"
)

Expand Down Expand Up @@ -734,13 +734,10 @@ def __repr__(self):
# This must not use `with_album=True`, because that might access
# the database. When debugging, that is not guaranteed to succeed, and
# can even deadlock due to the database lock.
return "{}({})".format(
type(self).__name__,
", ".join(
"{}={!r}".format(k, self[k])
for k in self.keys(with_album=False)
),
)
name = type(self).__name__
keys = self.keys(with_album=False)
fields = (f"{k}={repr(self[k])}" for k in keys)
return f"{name}({', '.join(fields)})"

def keys(self, computed=False, with_album=True):
"""Get a list of available field names.
Expand Down Expand Up @@ -1582,7 +1579,7 @@ def parse_query_string(s, model_cls):

The string is split into components using shell-like syntax.
"""
message = f"Query is not unicode: {s!r}"
message = f"Query is not unicode: {repr(s)}"
assert isinstance(s, str), message
try:
parts = shlex.split(s)
Expand Down
Loading