Skip to content

Commit d024ad9

Browse files
committed
Add pylint config, address findings, rename listdir filter→glob
pylint.toml existed as a 560-line --generate-toml-config dump. Moved the one non-default setting (py-version = "3.12") into pyproject.toml where it belongs and deleted the rest. Running pylint at 6.93/10 then actually fixing things: parse_string was one 64-statement, 27-branch function handling two completely different parsing modes. Extracted _parse_string_strict and _parse_string_unsafe as private helpers; parse_string is now a thin dispatcher. Should have been that way from the start. listdir's filter= parameter shadowed the built-in filter. Since we're in the 2.0 window anyway, renamed it to glob= (which is what it actually is). filter= still works with a DeprecationWarning pointing at the caller -- same pattern as parse_string_unsafe. Smaller fixes: - struct and warnings moved into unconditional top-level imports - fcntl: inline pylint disable with a note explaining why the platform guards above make it safe at that point - unit_class = None replaces an UnboundLocalError try/except used as flow control (ask me how I know that ends badly) - raise ... from None on StopIteration re-raises -- the internal exception is an implementation detail callers shouldn't see - dropped a try/except ValueError: raise that did nothing at all - removed unnecessary else clauses after returns in two places - cli_script gets a one-line docstring instead of a comment
1 parent 1e32f50 commit d024ad9

4 files changed

Lines changed: 159 additions & 117 deletions

File tree

bitmath/__init__.py

Lines changed: 118 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@
5050
import platform
5151
import re
5252
import shutil
53+
import struct
5354
import sys
5455
import threading
56+
import warnings
5557

5658
from collections.abc import Generator, Iterable, Iterator
5759
from typing import IO, Any, NamedTuple, Union
@@ -60,7 +62,6 @@
6062
if os.name == 'posix':
6163
import stat
6264
import fcntl
63-
import struct
6465
elif os.name == 'nt':
6566
import ctypes
6667
import ctypes.wintypes
@@ -1424,7 +1425,9 @@ def query_device_capacity(device_fd: IO[Any]) -> Byte:
14241425
# conditions for some possible errors. Really only for cases
14251426
# where it would add value to override the default exception
14261427
# message string.
1427-
buffer = fcntl.ioctl(device_fd.fileno(), request_code, b'\x00' * buffer_size)
1428+
buffer = fcntl.ioctl( # pylint: disable=possibly-used-before-assignment
1429+
device_fd.fileno(), request_code, b'\x00' * buffer_size
1430+
)
14281431

14291432
# Unpack the raw result from the ioctl call into a familiar
14301433
# python data type according to the ``fmt`` rules.
@@ -1513,17 +1516,17 @@ def getsize(path: str, bestprefix: bool = True, system: int = NIST) -> Bitmath:
15131516
size_bytes = os.path.getsize(_path)
15141517
if bestprefix:
15151518
return Byte(size_bytes).best_prefix(system=system)
1516-
else:
1517-
return Byte(size_bytes)
1519+
return Byte(size_bytes)
15181520

15191521

1520-
def listdir(
1522+
def listdir( # pylint: disable=too-many-arguments,too-many-positional-arguments
15211523
search_base: str,
15221524
followlinks: bool = False,
1523-
filter: str = '*',
1525+
glob: str = '*',
15241526
relpath: bool = False,
15251527
bestprefix: bool = False,
15261528
system: int = NIST,
1529+
**kwargs,
15271530
) -> Iterator[tuple[str, Bitmath]]:
15281531
"""This is a generator which recurses the directory tree
15291532
`search_base`, yielding 2-tuples of:
@@ -1533,7 +1536,7 @@ def listdir(
15331536
15341537
- `search_base` - The directory to begin walking down.
15351538
- `followlinks` - Whether or not to follow symbolic links to directories
1536-
- `filter` - A glob (see :py:mod:`fnmatch`) to filter results with
1539+
- `glob` - A glob (see :py:mod:`fnmatch`) to filter results with
15371540
(default: ``*``, everything)
15381541
- `relpath` - ``True`` to return the relative path from `pwd` or
15391542
``False`` (default) to return the fully qualified path
@@ -1547,8 +1550,18 @@ def listdir(
15471550
.. note:: Symlinks to **files** are followed automatically
15481551
15491552
"""
1550-
for root, dirs, files in os.walk(search_base, followlinks=followlinks):
1551-
for name in fnmatch.filter(files, filter):
1553+
if 'filter' in kwargs:
1554+
warnings.warn(
1555+
"The 'filter' parameter of listdir() is deprecated as of 2.0.0 and will be "
1556+
"removed in a future release. Use 'glob' instead.",
1557+
DeprecationWarning,
1558+
stacklevel=2,
1559+
)
1560+
glob = kwargs.pop('filter')
1561+
if kwargs:
1562+
raise TypeError(f"listdir() got unexpected keyword arguments: {list(kwargs)}")
1563+
for root, _, files in os.walk(search_base, followlinks=followlinks):
1564+
for name in fnmatch.filter(files, glob):
15521565
_path = os.path.join(root, name)
15531566
if relpath:
15541567
# RELATIVE path
@@ -1566,6 +1579,99 @@ def listdir(
15661579
yield (_return_path, getsize(_path, bestprefix=bestprefix, system=system))
15671580

15681581

1582+
def _parse_string_strict(s: str) -> 'Bitmath':
1583+
if not isinstance(s, str):
1584+
raise ValueError(f"parse_string only accepts string inputs but a {type(s)} was given")
1585+
1586+
# get the index of the first alphabetic character
1587+
try:
1588+
index = next(i for i, c in enumerate(s) if c.isalpha())
1589+
except StopIteration:
1590+
raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object") from None
1591+
1592+
# split the string into the value and the unit
1593+
val, unit = s[:index], s[index:]
1594+
1595+
# see if the unit exists as a type in our namespace
1596+
if unit == "b":
1597+
unit_class = Bit
1598+
elif unit == "B":
1599+
unit_class = Byte
1600+
else:
1601+
if not (hasattr(sys.modules[__name__], unit) and isinstance(getattr(sys.modules[__name__], unit), type)):
1602+
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1603+
unit_class = globals()[unit]
1604+
1605+
val = float(val)
1606+
return unit_class(val)
1607+
1608+
1609+
def _parse_string_unsafe(s: str | numbers.Number, system: int) -> 'Bitmath':
1610+
if not isinstance(s, str) and not isinstance(s, numbers.Number):
1611+
raise ValueError(f"parse_string only accepts string/number inputs but a {type(s)} was given")
1612+
1613+
# Test case: raw number input (easy!)
1614+
if isinstance(s, numbers.Number):
1615+
return Byte(s)
1616+
1617+
# Test case: a number pretending to be a string
1618+
if isinstance(s, str):
1619+
try:
1620+
return Byte(float(s))
1621+
except ValueError:
1622+
pass
1623+
1624+
# At this point the input is a string with a unit component.
1625+
# Separate the number and the unit.
1626+
try:
1627+
index = next(i for i, c in enumerate(s) if c.isalpha())
1628+
except StopIteration: # pragma: no cover
1629+
raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object") from None
1630+
1631+
val, unit = s[:index], s[index:]
1632+
1633+
# Explicit base-unit and word-form checks: handle B, b, bit(s),
1634+
# byte(s) before the prefix-normalization logic below.
1635+
_unit_lower = unit.lower()
1636+
if unit == 'B' or _unit_lower in ('byte', 'bytes'):
1637+
return Byte(float(val))
1638+
if unit == 'b' or _unit_lower in ('bit', 'bits'):
1639+
return Bit(float(val))
1640+
1641+
# Normalise: strip trailing b/B and append 'B' so we always
1642+
# work with byte-family units regardless of what was supplied.
1643+
unit = unit.rstrip('Bb')
1644+
unit += 'B'
1645+
1646+
unit_class = None
1647+
if len(unit) == 2:
1648+
if system == NIST:
1649+
unit = capitalize_first(unit)
1650+
_unit = list(unit)
1651+
_unit.insert(1, 'i')
1652+
unit = ''.join(_unit)
1653+
if unit in globals():
1654+
unit_class = globals()[unit]
1655+
else:
1656+
if unit.startswith('K'):
1657+
unit = unit.replace('K', 'k')
1658+
elif not unit.startswith('k'):
1659+
unit = capitalize_first(unit)
1660+
if unit[0] in SI_PREFIXES:
1661+
unit_class = globals()[unit]
1662+
elif len(unit) == 3:
1663+
unit = capitalize_first(unit)
1664+
if unit[:2] in NIST_PREFIXES:
1665+
unit_class = globals()[unit]
1666+
else:
1667+
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1668+
1669+
if unit_class is None:
1670+
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1671+
1672+
return unit_class(float(val))
1673+
1674+
15691675
def parse_string(s: str | numbers.Number, system: int = NIST, strict: bool = True) -> Bitmath:
15701676
"""Parse a string with units and return a bitmath instance.
15711677
@@ -1606,102 +1712,8 @@ def parse_string(s: str | numbers.Number, system: int = NIST, strict: bool = Tru
16061712
defaults to ``bitmath.NIST`` and is ignored when ``strict=True``.
16071713
"""
16081714
if strict:
1609-
# Strings only please
1610-
if not isinstance(s, str):
1611-
raise ValueError(f"parse_string only accepts string inputs but a {type(s)} was given")
1612-
1613-
# get the index of the first alphabetic character
1614-
try:
1615-
index = next(i for i, c in enumerate(s) if c.isalpha())
1616-
except StopIteration:
1617-
# If there's no alphabetic characters we won't be able to find a match
1618-
raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object")
1619-
1620-
# split the string into the value and the unit
1621-
val, unit = s[:index], s[index:]
1622-
1623-
# see if the unit exists as a type in our namespace
1624-
if unit == "b":
1625-
unit_class = Bit
1626-
elif unit == "B":
1627-
unit_class = Byte
1628-
else:
1629-
if not (hasattr(sys.modules[__name__], unit) and isinstance(getattr(sys.modules[__name__], unit), type)):
1630-
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1631-
unit_class = globals()[unit]
1632-
1633-
try:
1634-
val = float(val)
1635-
except ValueError:
1636-
raise
1637-
return unit_class(val)
1638-
1639-
else:
1640-
# strict=False path (formerly parse_string_unsafe)
1641-
if not isinstance(s, str) and not isinstance(s, numbers.Number):
1642-
raise ValueError(f"parse_string only accepts string/number inputs but a {type(s)} was given")
1643-
1644-
# Test case: raw number input (easy!)
1645-
if isinstance(s, numbers.Number):
1646-
return Byte(s)
1647-
1648-
# Test case: a number pretending to be a string
1649-
if isinstance(s, str):
1650-
try:
1651-
return Byte(float(s))
1652-
except ValueError:
1653-
pass
1654-
1655-
# At this point the input is a string with a unit component.
1656-
# Separate the number and the unit.
1657-
try:
1658-
index = next(i for i, c in enumerate(s) if c.isalpha())
1659-
except StopIteration: # pragma: no cover
1660-
raise ValueError(f"No unit detected, can not parse string '{s}' into a bitmath object")
1661-
1662-
val, unit = s[:index], s[index:]
1663-
1664-
# Explicit base-unit and word-form checks: handle B, b, bit(s),
1665-
# byte(s) before the prefix-normalization logic below.
1666-
_unit_lower = unit.lower()
1667-
if unit == 'B' or _unit_lower in ('byte', 'bytes'):
1668-
return Byte(float(val))
1669-
if unit == 'b' or _unit_lower in ('bit', 'bits'):
1670-
return Bit(float(val))
1671-
1672-
# Normalise: strip trailing b/B and append 'B' so we always
1673-
# work with byte-family units regardless of what was supplied.
1674-
unit = unit.rstrip('Bb')
1675-
unit += 'B'
1676-
1677-
if len(unit) == 2:
1678-
if system == NIST:
1679-
unit = capitalize_first(unit)
1680-
_unit = list(unit)
1681-
_unit.insert(1, 'i')
1682-
unit = ''.join(_unit)
1683-
if unit in globals():
1684-
unit_class = globals()[unit]
1685-
else:
1686-
if unit.startswith('K'):
1687-
unit = unit.replace('K', 'k')
1688-
elif not unit.startswith('k'):
1689-
unit = capitalize_first(unit)
1690-
if unit[0] in SI_PREFIXES:
1691-
unit_class = globals()[unit]
1692-
elif len(unit) == 3:
1693-
unit = capitalize_first(unit)
1694-
if unit[:2] in NIST_PREFIXES:
1695-
unit_class = globals()[unit]
1696-
else:
1697-
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1698-
1699-
try:
1700-
unit_class
1701-
except UnboundLocalError:
1702-
raise ValueError(f"The unit {unit} is not a valid bitmath unit")
1703-
1704-
return unit_class(float(val))
1715+
return _parse_string_strict(s)
1716+
return _parse_string_unsafe(s, system)
17051717

17061718

17071719
def parse_string_unsafe(s: str | numbers.Number, system: int = NIST) -> Bitmath:
@@ -1718,7 +1730,6 @@ def parse_string_unsafe(s: str | numbers.Number, system: int = NIST) -> Bitmath:
17181730
warnings.filterwarnings('ignore', category=DeprecationWarning,
17191731
module='bitmath')
17201732
"""
1721-
import warnings
17221733
warnings.warn(
17231734
"parse_string_unsafe is deprecated as of 2.0.0 and will be removed "
17241735
"in a future release. Use parse_string(s, strict=False, system=system) "
@@ -1842,8 +1853,7 @@ def cli_script_main(cli_args):
18421853

18431854

18441855
def cli_script(): # pragma: no cover
1845-
# Wrapper around cli_script_main so we can unittest the command
1846-
# line functionality
1856+
"""Entry point for the bitmath CLI; wraps cli_script_main for testability."""
18471857
for result in cli_script_main(sys.argv[1:]):
18481858
print(result)
18491859

docsite/source/module.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ bitmath.getsize()
8383
bitmath.listdir()
8484
=================
8585

86-
.. function:: listdir(search_base[, followlinks=False[, filter='*'[, relpath=False[, bestprefix=False[, system=NIST]]]]])
86+
.. function:: listdir(search_base[, followlinks=False[, glob='*'[, relpath=False[, bestprefix=False[, system=NIST]]]]])
8787

8888
This is a `generator
8989
<https://docs.python.org/3/tutorial/classes.html#generators>`_
@@ -97,10 +97,10 @@ bitmath.listdir()
9797
links. Whether or not to follow symbolic
9898
links to directories. Setting to ``True``
9999
enables directory link following
100-
:param string filter: **Default:** ``*`` (everything). A glob to
101-
filter results with. See `fnmatch
102-
<https://docs.python.org/3/library/fnmatch.html>`_
103-
for more details about *globs*
100+
:param string glob: **Default:** ``*`` (everything). A glob to
101+
filter results with. See `fnmatch
102+
<https://docs.python.org/3/library/fnmatch.html>`_
103+
for more details about *globs*
104104
:param bool relpath: **Default:** ``False``, returns the fully
105105
qualified to each discovered file. ``True`` to
106106
return the relative path from the present
@@ -195,12 +195,12 @@ bitmath.listdir()
195195
on lines **10** and **11** the path is relative to the present
196196
working directory.
197197

198-
Let's play with the ``filter`` parameter now. Let's say we only
198+
Let's play with the ``glob`` parameter now. Let's say we only
199199
want to include results for files whose name begins with "second":
200200

201201
.. code-block:: python
202202
203-
>>> for f in bitmath.listdir('./some_files', filter='second*'):
203+
>>> for f in bitmath.listdir('./some_files', glob='second*'):
204204
... print(f)
205205
...
206206
('/tmp/tmp.P5lqtyqwPh/some_files/deeper_files/second_file', Byte(13370.0))

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,6 @@ packages = ["bitmath"]
8080

8181
[tool.hatch.publish.index]
8282
disable = true
83+
84+
[tool.pylint.main]
85+
py-version = "3.12"

0 commit comments

Comments
 (0)