diff --git a/my/bluemaestro.py b/my/bluemaestro.py index 50338bb0..5d0968bb 100644 --- a/my/bluemaestro.py +++ b/my/bluemaestro.py @@ -153,7 +153,7 @@ def measurements() -> Iterable[Res[Measurement]]: oldfmt = False db_dt = None - for i, (name, tsc, temp, hum, pres, dewp) in enumerate(datas): + for (name, tsc, temp, hum, pres, dewp) in datas: if is_bad_table(name): continue diff --git a/my/body/exercise/cross_trainer.py b/my/body/exercise/cross_trainer.py index d073f43d..edbb5571 100644 --- a/my/body/exercise/cross_trainer.py +++ b/my/body/exercise/cross_trainer.py @@ -105,7 +105,7 @@ def dataframe() -> DataFrameT: rows = [] idxs = [] # type: ignore[var-annotated] NO_ENDOMONDO = 'no endomondo matches' - for i, row in mdf.iterrows(): + for _i, row in mdf.iterrows(): rd = row.to_dict() mdate = row['date'] if pd.isna(mdate): diff --git a/my/coding/github.py b/my/coding/github.py index 9358b047..de64f054 100644 --- a/my/coding/github.py +++ b/my/coding/github.py @@ -1,9 +1,12 @@ -import warnings +from typing import TYPE_CHECKING -warnings.warn('my.coding.github is deprecated! Please use my.github.all instead!') +from my.core import warnings + +warnings.high('my.coding.github is deprecated! Please use my.github.all instead!') # todo why aren't DeprecationWarning shown by default?? -from ..github.all import events, get_events +if not TYPE_CHECKING: + from ..github.all import events, get_events -# todo deprecate properly -iter_events = events + # todo deprecate properly + iter_events = events diff --git a/my/core/__main__.py b/my/core/__main__.py index 3af8e08d..c5e45521 100644 --- a/my/core/__main__.py +++ b/my/core/__main__.py @@ -456,9 +456,9 @@ def _locate_functions_or_prompt(qualified_names: List[str], *, prompt: bool = Tr # user to select a 'data provider' like function try: mod = importlib.import_module(qualname) - except Exception: + except Exception as ie: eprint(f"During fallback, importing '{qualname}' as module failed") - raise qr_err + raise qr_err from ie # find data providers in this module data_providers = [f for _, f in inspect.getmembers(mod, inspect.isfunction) if is_data_provider(f)] diff --git a/my/core/cachew.py b/my/core/cachew.py index e0e7adf8..dc6ed79b 100644 --- a/my/core/cachew.py +++ b/my/core/cachew.py @@ -2,7 +2,6 @@ import logging import sys -import warnings from contextlib import contextmanager from pathlib import Path from typing import ( @@ -20,6 +19,9 @@ import appdirs # type: ignore[import-untyped] +from . import warnings + + PathIsh = Union[str, Path] # avoid circular import from .common @@ -116,7 +118,7 @@ def _mcachew_impl(cache_path=_cache_path_dflt, **kwargs): try: import cachew except ModuleNotFoundError: - warnings.warn('cachew library not found. You might want to install it to speed things up. See https://github.com/karlicoss/cachew') + warnings.high('cachew library not found. You might want to install it to speed things up. See https://github.com/karlicoss/cachew') return lambda orig_func: orig_func else: kwargs['cache_path'] = cache_path diff --git a/my/core/common.py b/my/core/common.py index b97866f5..5f8d03a9 100644 --- a/my/core/common.py +++ b/my/core/common.py @@ -1,5 +1,4 @@ import os -import warnings from glob import glob as do_glob from pathlib import Path from typing import ( @@ -15,7 +14,7 @@ ) from . import compat -from . import warnings as core_warnings +from . import warnings as warnings # some helper functions # TODO start deprecating this? soon we'd be able to use Path | str syntax which is shorter and more explicit @@ -63,7 +62,7 @@ def caller() -> str: gs = str(src) if '*' in gs: if glob != DEFAULT_GLOB: - warnings.warn(f"{caller()}: treating {gs} as glob path. Explicit glob={glob} argument is ignored!") + warnings.medium(f"{caller()}: treating {gs} as glob path. Explicit glob={glob} argument is ignored!") paths.extend(map(Path, do_glob(gs))) elif os.path.isdir(str(src)): # NOTE: we're using os.path here on purpose instead of src.is_dir @@ -85,7 +84,7 @@ def caller() -> str: if len(paths) == 0: # todo make it conditionally defensive based on some global settings - core_warnings.high(f''' + warnings.high(f''' {caller()}: no paths were matched against {pp}. This might result in missing data. Likely, the directory you passed is empty. '''.strip()) # traceback is useful to figure out what config caused it? diff --git a/my/core/error.py b/my/core/error.py index cd8d093e..e869614c 100644 --- a/my/core/error.py +++ b/my/core/error.py @@ -119,7 +119,7 @@ def sort_res_by(items: Iterable[Res[T]], key: Callable[[Any], K]) -> List[Res[T] group = [] results: List[Res[T]] = [] - for v, grp in sorted(groups, key=lambda p: p[0]): # type: ignore[return-value, arg-type] # TODO SupportsLessThan?? + for _v, grp in sorted(groups, key=lambda p: p[0]): # type: ignore[return-value, arg-type] # TODO SupportsLessThan?? results.extend(grp) results.extend(group) # handle last group (it will always be errors only) diff --git a/my/core/hpi_compat.py b/my/core/hpi_compat.py index bad0b170..9330e495 100644 --- a/my/core/hpi_compat.py +++ b/my/core/hpi_compat.py @@ -6,7 +6,7 @@ import os import re from types import ModuleType -from typing import Iterator, List, Optional, TypeVar +from typing import Iterator, List, Optional, Sequence, TypeVar from . import warnings @@ -71,7 +71,7 @@ def pre_pip_dal_handler( name: str, e: ModuleNotFoundError, cfg, - requires=[], + requires: Sequence[str] = (), ) -> ModuleType: ''' https://github.com/karlicoss/HPI/issues/79 diff --git a/my/core/init.py b/my/core/init.py index 49148de4..7a30955b 100644 --- a/my/core/init.py +++ b/my/core/init.py @@ -25,7 +25,7 @@ def setup_config() -> None: warnings.warn(f""" 'my.config' package isn't found! (expected at '{mycfg_dir}'). This is likely to result in issues. See https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#setting-up-the-modules for more info. -""".strip()) +""".strip(), stacklevel=1) return mpath = str(mycfg_dir) @@ -47,7 +47,7 @@ def setup_config() -> None: warnings.warn(f""" Importing 'my.config' failed! (error: {ex}). This is likely to result in issues. See https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#setting-up-the-modules for more info. -""") +""", stacklevel=1) else: # defensive just in case -- __file__ may not be present if there is some dynamic magic involved used_config_file = getattr(my.config, '__file__', None) @@ -63,7 +63,7 @@ def setup_config() -> None: Expected my.config to be located at {mycfg_dir}, but instead its path is {used_config_path}. This will likely cause issues down the line -- double check {mycfg_dir} structure. See https://github.com/karlicoss/HPI/blob/master/doc/SETUP.org#setting-up-the-modules for more info. -""", +""", stacklevel=1 ) diff --git a/my/core/logging.py b/my/core/logging.py index 734c1e0a..bdee9aaf 100644 --- a/my/core/logging.py +++ b/my/core/logging.py @@ -15,7 +15,7 @@ def test() -> None: ## prepare exception for later try: - None.whatever # type: ignore[attr-defined] + None.whatever # type: ignore[attr-defined] # noqa: B018 except Exception as e: ex = e ## @@ -146,7 +146,7 @@ def _setup_handlers_and_formatters(name: str) -> None: # try colorlog first, so user gets nice colored logs import colorlog except ModuleNotFoundError: - warnings.warn("You might want to 'pip install colorlog' for nice colored logs") + warnings.warn("You might want to 'pip install colorlog' for nice colored logs", stacklevel=1) formatter = logging.Formatter(FORMAT_NOCOLOR) else: # log_color/reset are specific to colorlog @@ -233,7 +233,7 @@ def get_enlighten(): try: import enlighten # type: ignore[import-untyped] except ModuleNotFoundError: - warnings.warn("You might want to 'pip install enlighten' for a nice progress bar") + warnings.warn("You might want to 'pip install enlighten' for a nice progress bar", stacklevel=1) return Mock() diff --git a/my/core/orgmode.py b/my/core/orgmode.py index d9a254c4..c70ded6a 100644 --- a/my/core/orgmode.py +++ b/my/core/orgmode.py @@ -6,12 +6,12 @@ def parse_org_datetime(s: str) -> datetime: s = s.strip('[]') - for fmt, cl in [ - ("%Y-%m-%d %a %H:%M", datetime), - ("%Y-%m-%d %H:%M" , datetime), - # todo not sure about these... fallback on 00:00? - # ("%Y-%m-%d %a" , date), - # ("%Y-%m-%d" , date), + for fmt, _cls in [ + ("%Y-%m-%d %a %H:%M", datetime), + ("%Y-%m-%d %H:%M" , datetime), + # todo not sure about these... fallback on 00:00? + # ("%Y-%m-%d %a" , date), + # ("%Y-%m-%d" , date), ]: try: return datetime.strptime(s, fmt) diff --git a/my/core/query.py b/my/core/query.py index daf702d9..c337e5c4 100644 --- a/my/core/query.py +++ b/my/core/query.py @@ -72,7 +72,7 @@ def locate_function(module_name: str, function_name: str) -> Callable[[], Iterab if func is not None and callable(func): return func except Exception as e: - raise QueryException(str(e)) + raise QueryException(str(e)) # noqa: B904 raise QueryException(f"Could not find function '{function_name}' in '{module_name}'") @@ -468,7 +468,7 @@ def select( try: itr: Iterator[ET] = iter(it) except TypeError as t: - raise QueryException("Could not convert input src to an Iterator: " + str(t)) + raise QueryException("Could not convert input src to an Iterator: " + str(t)) # noqa: B904 # if both drop_exceptions and drop_exceptions are provided for some reason, # should raise exceptions before dropping them diff --git a/my/core/query_range.py b/my/core/query_range.py index 761b0457..0a1b3217 100644 --- a/my/core/query_range.py +++ b/my/core/query_range.py @@ -109,7 +109,7 @@ def _datelike_to_float(dl: Any) -> float: try: return parse_datetime_float(dl) except QueryException as q: - raise QueryException(f"While attempting to extract datetime from {dl}, to order by datetime:\n\n" + str(q)) + raise QueryException(f"While attempting to extract datetime from {dl}, to order by datetime:\n\n" + str(q)) # noqa: B904 class RangeTuple(NamedTuple): diff --git a/my/core/source.py b/my/core/source.py index 4510ef02..52c58c1f 100644 --- a/my/core/source.py +++ b/my/core/source.py @@ -62,7 +62,7 @@ def wrapper(*args, **kwargs) -> Iterator[T]: class core: disabled_modules = [{module_name!r}] -""") +""", stacklevel=1) # try to check if this is a config error or based on dependencies not being installed if isinstance(err, (ImportError, AttributeError)): matched_config_err = warn_my_config_import_error(err, module_name=module_name, help_url=help_url) diff --git a/my/core/stats.py b/my/core/stats.py index 4c9fb0c6..aa05355c 100644 --- a/my/core/stats.py +++ b/my/core/stats.py @@ -440,7 +440,7 @@ def _guess_datetime(x: Any) -> Optional[datetime]: d = asdict(x) except: # noqa: E722 bare except return None - for k, v in d.items(): + for _k, v in d.items(): if isinstance(v, datetime): return v return None diff --git a/my/core/util.py b/my/core/util.py index b49acf60..fb3edf84 100644 --- a/my/core/util.py +++ b/my/core/util.py @@ -93,11 +93,11 @@ def _discover_path_importables(pkg_pth: Path, pkg_name: str) -> Iterable[HPIModu def _walk_packages(path: Iterable[str], prefix: str='', onerror=None) -> Iterable[HPIModule]: """ Modified version of https://github.com/python/cpython/blob/d50a0700265536a20bcce3fb108c954746d97625/Lib/pkgutil.py#L53, - to alvoid importing modules that are skipped + to avoid importing modules that are skipped """ from .core_config import config - def seen(p, m={}): + def seen(p, m={}): # noqa: B006 if p in m: return True m[p] = True diff --git a/my/core/utils/itertools.py b/my/core/utils/itertools.py index 66f82bd4..b945ad82 100644 --- a/my/core/utils/itertools.py +++ b/my/core/utils/itertools.py @@ -24,6 +24,8 @@ from decorator import decorator from ..compat import ParamSpec +from .. import warnings as core_warnings + T = TypeVar('T') K = TypeVar('K') @@ -142,8 +144,7 @@ def _warn_if_empty(func, *args, **kwargs): if isinstance(iterable, Sized): sz = len(iterable) if sz == 0: - # todo use hpi warnings here? - warnings.warn(f"Function {func} returned empty container, make sure your config paths are correct") + core_warnings.medium(f"Function {func} returned empty container, make sure your config paths are correct") return iterable else: # must be an iterator @@ -153,7 +154,7 @@ def wit(): yield i empty = False if empty: - warnings.warn(f"Function {func} didn't emit any data, make sure your config paths are correct") + core_warnings.medium(f"Function {func} didn't emit any data, make sure your config paths are correct") return wit() diff --git a/my/core/warnings.py b/my/core/warnings.py index 82e539ba..2ffc3e4d 100644 --- a/my/core/warnings.py +++ b/my/core/warnings.py @@ -12,7 +12,7 @@ import click -def _colorize(x: str, color: Optional[str]=None) -> str: +def _colorize(x: str, color: Optional[str] = None) -> str: if color is None: return x @@ -24,10 +24,10 @@ def _colorize(x: str, color: Optional[str]=None) -> str: return click.style(x, fg=color) -def _warn(message: str, *args, color: Optional[str]=None, **kwargs) -> None: +def _warn(message: str, *args, color: Optional[str] = None, **kwargs) -> None: stacklevel = kwargs.get('stacklevel', 1) - kwargs['stacklevel'] = stacklevel + 2 # +1 for this function, +1 for medium/high wrapper - warnings.warn(_colorize(message, color=color), *args, **kwargs) + kwargs['stacklevel'] = stacklevel + 2 # +1 for this function, +1 for medium/high wrapper + warnings.warn(_colorize(message, color=color), *args, **kwargs) # noqa: B028 def low(message: str, *args, **kwargs) -> None: @@ -55,4 +55,4 @@ def high(message: str, *args, **kwargs) -> None: def warn(*args, **kwargs): import warnings - return warnings.warn(*args, **kwargs) + return warnings.warn(*args, **kwargs) # noqa: B028 diff --git a/my/jawbone/__init__.py b/my/jawbone/__init__.py index 1706a54e..affe2303 100644 --- a/my/jawbone/__init__.py +++ b/my/jawbone/__init__.py @@ -274,7 +274,7 @@ def plot() -> None: fig: Figure = plt.figure(figsize=(15, sleeps_count * 1)) axarr = fig.subplots(nrows=len(sleeps)) - for i, (sleep, axes) in enumerate(zip(sleeps, axarr)): + for (sleep, axes) in zip(sleeps, axarr): plot_one(sleep, fig, axes, showtext=True) used = melatonin_data.get(sleep.date_, None) sused: str diff --git a/my/location/google.py b/my/location/google.py index a7a92d33..b966ec6d 100644 --- a/my/location/google.py +++ b/my/location/google.py @@ -22,9 +22,10 @@ from my.core import stat, Stats, make_logger from my.core.cachew import cache_dir, mcachew -from my.core.warnings import high +from my.core import warnings -high("Please set up my.google.takeout.parser module for better takeout support") + +warnings.high("Please set up my.google.takeout.parser module for better takeout support") # otherwise uses ijson @@ -52,8 +53,7 @@ def _iter_via_ijson(fo) -> Iterable[TsLatLon]: # pip3 install ijson cffi import ijson.backends.yajl2_cffi as ijson # type: ignore except: - import warnings - warnings.warn("Falling back to default ijson because 'cffi' backend isn't found. It's up to 2x faster, you might want to check it out") + warnings.medium("Falling back to default ijson because 'cffi' backend isn't found. It's up to 2x faster, you might want to check it out") import ijson # type: ignore for d in ijson.items(fo, 'locations.item'): @@ -105,7 +105,8 @@ def _iter_locations_fo(fit) -> Iterable[Location]: errors += 1 if float(errors) / total > 0.01: # todo make defensive? - raise RuntimeError('too many errors! aborting') + # todo exceptiongroup? + raise RuntimeError('too many errors! aborting') # noqa: B904 else: continue diff --git a/my/media/imdb.py b/my/media/imdb.py index df6d62d5..c66f5dc2 100644 --- a/my/media/imdb.py +++ b/my/media/imdb.py @@ -22,7 +22,7 @@ def iter_movies() -> Iterator[Movie]: with last.open() as fo: reader = csv.DictReader(fo) - for i, line in enumerate(reader): + for line in reader: # TODO extract directors?? title = line['Title'] rating = int(line['You rated']) diff --git a/my/polar.py b/my/polar.py index 197de180..9125f17b 100644 --- a/my/polar.py +++ b/my/polar.py @@ -166,7 +166,7 @@ def load_item(self, meta: Zoomable) -> Iterable[Highlight]: htags: List[str] = [] if 'tags' in h: ht = h['tags'].zoom() - for k, v in list(ht.items()): + for _k, v in list(ht.items()): ctag = v.zoom() ctag['id'].consume() ct = ctag['label'].zoom() @@ -199,7 +199,7 @@ def load_item(self, meta: Zoomable) -> Iterable[Highlight]: def load_items(self, metas: Json) -> Iterable[Highlight]: - for p, meta in metas.items(): + for _p, meta in metas.items(): with wrap(meta, throw=not config.defensive) as meta: yield from self.load_item(meta) diff --git a/my/reddit/rexport.py b/my/reddit/rexport.py index 6a6be617..5dcd7d92 100644 --- a/my/reddit/rexport.py +++ b/my/reddit/rexport.py @@ -144,9 +144,9 @@ def upvoted() -> Iterator[Upvote]: try: # here we just check that types are available, we don't actually want to import them # fmt: off - dal.Subreddit - dal.Profile - dal.Multireddit + dal.Subreddit # noqa: B018 + dal.Profil # noqa: B018e + dal.Multireddit # noqa: B018 # fmt: on except AttributeError as ae: warnings.high(f'{ae} : please update "rexport" installation') diff --git a/my/rss/common.py b/my/rss/common.py index 54067d6d..bb752970 100644 --- a/my/rss/common.py +++ b/my/rss/common.py @@ -32,7 +32,7 @@ def compute_subscriptions(*sources: Iterable[SubscriptionState]) -> List[Subscri by_url: Dict[str, Subscription] = {} # ah. dates are used for sorting - for when, state in sorted(states): + for _when, state in sorted(states): # TODO use 'when'? for feed in state: if feed.url not in by_url: diff --git a/my/tests/location/google.py b/my/tests/location/google.py index 612522bc..43b86464 100644 --- a/my/tests/location/google.py +++ b/my/tests/location/google.py @@ -44,8 +44,8 @@ def _prepare_takeouts_dir(tmp_path: Path) -> Path: try: track = one(testdata().rglob('italy-slovenia-2017-07-29.json')) - except ValueError: - raise RuntimeError('testdata not found, setup git submodules?') + except ValueError as e: + raise RuntimeError('testdata not found, setup git submodules?') from e # todo ugh. unnecessary zipping, but at the moment takeout provider doesn't support plain dirs import zipfile diff --git a/my/tests/shared_tz_config.py b/my/tests/shared_tz_config.py index 3d95a9ef..810d989b 100644 --- a/my/tests/shared_tz_config.py +++ b/my/tests/shared_tz_config.py @@ -49,8 +49,8 @@ def _prepare_takeouts_dir(tmp_path: Path) -> Path: try: track = one(testdata().rglob('italy-slovenia-2017-07-29.json')) - except ValueError: - raise RuntimeError('testdata not found, setup git submodules?') + except ValueError as e: + raise RuntimeError('testdata not found, setup git submodules?') from e # todo ugh. unnecessary zipping, but at the moment takeout provider doesn't support plain dirs import zipfile diff --git a/my/time/tz/common.py b/my/time/tz/common.py index 89150c78..13c8ac0f 100644 --- a/my/time/tz/common.py +++ b/my/time/tz/common.py @@ -33,7 +33,7 @@ def default_policy() -> TzPolicy: def localize_with_policy( lfun: Callable[[datetime], datetime_aware], dt: datetime, - policy: TzPolicy=default_policy() + policy: TzPolicy=default_policy() # noqa: B008 ) -> datetime_aware: tz = dt.tzinfo if tz is None: diff --git a/my/twitter/archive.py b/my/twitter/archive.py index 685f7fc1..d326d703 100644 --- a/my/twitter/archive.py +++ b/my/twitter/archive.py @@ -14,9 +14,9 @@ try: from my.config import twitter as user_config # type: ignore[assignment] except ImportError: - raise ie # raise the original exception.. must be something else + raise ie # raise the original exception.. must be something else # noqa: B904 else: - from ..core import warnings + from my.core import warnings warnings.high('my.config.twitter is deprecated! Please rename it to my.config.twitter_archive in your config') ## diff --git a/ruff.toml b/ruff.toml index 69af75af..3d97fc92 100644 --- a/ruff.toml +++ b/ruff.toml @@ -7,8 +7,11 @@ lint.extend-select = [ "UP", # detect deprecated python stdlib stuff "FBT", # detect use of boolean arguments "RUF", # various ruff-specific rules + "PLR", # 'refactor' rules + "B", # 'bugbear' set -- various possible bugs + + - "PLR", # "S", # bandit (security checks) -- tends to be not very useful, lots of nitpicks # "DTZ", # datetimes checks -- complaining about missing tz and mostly false positives ] @@ -57,4 +60,9 @@ lint.ignore = [ "PLR2004", # magic value in comparison -- super annoying in tests ### "PLR0402", # import X.Y as Y -- TODO maybe consider enabling it, but double check + + "B009", # calling gettattr with constant attribute -- this is useful to convince mypy + "B010", # same as above, but setattr + "B017", # pytest.raises(Exception) + "B023", # seems to result in false positives? ] diff --git a/tests/github.py b/tests/github.py index 6b7df232..ed890533 100644 --- a/tests/github.py +++ b/tests/github.py @@ -5,11 +5,13 @@ def test_gdpr() -> None: import my.github.gdpr as gdpr + assert ilen(gdpr.events()) > 100 def test() -> None: - from my.coding.github import get_events + from my.github.all import get_events + events = get_events() assert ilen(events) > 100 for e in events: