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

DM-31722: Migrate utility code from other packages #100

Merged
merged 81 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
da18eba
Add new task logging class based on python logging
tgoldina Jul 8, 2021
bbec3d2
Switch to LoggingAdapter
timj Jul 9, 2021
73d5785
Add support for getChild to the logger
timj Jul 9, 2021
56576be
Drop Lsst name in log adapter
timj Jul 19, 2021
b9ba2f6
Remove configure_prop compatibility method
timj Jul 19, 2021
1664fb8
Add verbose and trap large log levels
timj Jul 19, 2021
3e1e076
Add context manager to temporarily change logger level
timj Jul 19, 2021
fd823fe
Add VERBOSE and TRACE and fatal() support
timj Jul 19, 2021
7f9faa3
Set stacklevel for forwarded log messages
timj Jul 26, 2021
657a4a4
Move log constants to class properties
timj Jul 26, 2021
3bba01a
Remove the flag to always return a python Logger
timj Jul 26, 2021
9790b33
Do not deprecate log.fatal()
timj Jul 26, 2021
80a04c5
Export TaskLogAdapter and getLogger by default
timj Jul 26, 2021
06795b0
Improve documentation
timj Jul 26, 2021
f29d626
Use TRACE instead of hardcoded 5
timj Jul 26, 2021
acc3410
Rename getLogger to getTaskLogger
timj Jul 26, 2021
eb5b836
Remove unused import of lsst.log
timj Jul 27, 2021
28dc5fc
Add simple test for new python-based Task logging
timj Jul 26, 2021
26babb1
Adjust naming to match new location in utils package
timj Sep 21, 2021
b41d474
Add some additional tests of the logger class
timj Sep 21, 2021
5d57dce
Fix license to match agreed form.
timj Sep 21, 2021
0f1dd78
Fix license to match agreed form
timj Sep 21, 2021
fec68e0
Add logging module to docs
timj Sep 22, 2021
ce1e5f6
Added a timer context. Modified both timers to add instead of set, so…
Nov 10, 2011
73e68f8
Added a preliminary argument parser. Modified the timer to measure CP…
Nov 11, 2011
0695c28
Made timer a method of Task instead of a standalone function so the r…
r-owen Dec 1, 2011
7adde8f
Add handleCommand method to ArgumentParser. Add lots of resource logg…
r-owen Feb 13, 2012
1078bcd
fix ticket #1912: not logging end of method if it fails
r-owen Feb 14, 2012
178a9c4
Change default log level from INFO to DEBUG for timing and resource u…
r-owen Feb 16, 2012
e88d747
Handle exceptions in PropertyList.add in timer.py
r-owen Apr 9, 2012
2e2fc8d
Make all resource values long in the timer to avoid problems adding t…
r-owen Apr 10, 2012
9c37bb0
Fix a doc comment
r-owen Apr 27, 2012
a4cde20
Improved the documentation, including documenting the PIPE_INPUT_ROOT…
r-owen Aug 31, 2012
8e59b89
Change log.log(level, msg) to log.level(msg). Refactor parse_args sli…
r-owen Oct 16, 2012
8179a81
Squashed merge of tickets/2420: better names for timing metadata, and…
PaulPrice Nov 3, 2012
b43b65e
Implemented ticket 2914 Updated argument parser, documentation and un…
r-owen Oct 25, 2013
d84c0bd
Cleaned up documentation for the various classes.
r-owen Aug 6, 2014
13b2bad
Updated for Paul's thorough review
r-owen Aug 7, 2014
4735238
Fix pyflakes warnings
r-owen Jun 4, 2016
1448d6f
Ran autopep8 on all Python code
r-owen Aug 17, 2016
bb3cc31
Ran futurize -1 -w .
r-owen Aug 17, 2016
40366a9
Run futurize -2 -x division_safe -w .
r-owen Aug 17, 2016
64d565d
Standardize order and grouping of import statements
r-owen Aug 20, 2016
da955df
Switch Tasks' logs from lsst.pex.logging to lsst.log per RFC-203
Apr 20, 2016
027c70a
Remove use of private lsst.log API
timj Jan 19, 2017
422fe35
Convert timer module docstrings to numpydoc
jonathansick Jul 30, 2017
594e21d
Make time/resource metadata explicitly LongLong.
ktlim Apr 4, 2018
1f614b5
Remove `from __future__ import...`
r-owen Aug 15, 2018
176cd8e
Move __all__ to the preferred location
r-owen Aug 15, 2018
95d6ab4
Remove unused variable
timj Oct 24, 2018
fab7132
Replace deprecated time.clock() with time.process_time()
parejkoj Apr 2, 2019
41720b6
Use f strings everywhere
r-owen Apr 9, 2020
ddc9914
Make a sublogger for timer messages.
ktlim May 21, 2020
3c28116
Make docstrings compliant with coding standard line length
timj Oct 9, 2020
3b4b4b8
Make timing utilities easier to use outside of a Task.
TallJimbo Jun 9, 2021
4047713
Switch timer to use python logging
tgoldina Jul 8, 2021
8dd5a30
Use agreed license text
timj Sep 22, 2021
468a34e
Make timeMethod and others more generic
timj Sep 28, 2021
6f06cfe
Add type annotations to timer.py and logging.py
timj Sep 28, 2021
c638575
Transfer lsst.daf.butler.core.utils.time_this to lsst.utils.timer
timj Oct 1, 2021
aba4c60
Transfer iteration utilities from daf_butler
timj Oct 1, 2021
a608e3d
Move getFullTypeName and related utilities over from lsst.daf.butler
timj Oct 1, 2021
3c77b45
Improve tests of get_instance_of with modules
timj Oct 1, 2021
743ac1f
Move get_caller_name to lsst.utils.introspection
timj Oct 1, 2021
3685c49
Use zip_longest in test to exhaust generator
timj Oct 1, 2021
e660f76
Add py.typed file to the package for mypy compatibility
timj Oct 4, 2021
5b635d2
Allow python Logger and LSST Logger in time_this
timj Oct 5, 2021
54d3b37
Add doImportType that guarantees to return a Type
timj Oct 5, 2021
7d71552
Add additional parameter check to isplit
timj Oct 5, 2021
969e0ab
Add test for doImportType
timj Oct 5, 2021
b8e6736
Add helper code for setting up python classes
timj Oct 5, 2021
a2efb26
Add news fragment
timj Oct 5, 2021
e12c770
Add some v23 news fragments
timj Oct 5, 2021
63191b1
Backfill some older release notes
timj Oct 5, 2021
053beb4
Add new packages to docs
timj Oct 5, 2021
1f044c1
Fix some docstrings
timj Oct 5, 2021
ede4ee2
Change the release notes to link to the relevant function
timj Oct 5, 2021
bde6133
Rename iteration.iterable to iteration.ensure_iterable
timj Oct 6, 2021
d117918
Ensure that logInfo reports the line from non-utils code
timj Oct 7, 2021
6d6b01a
Minor grammar fixes
timj Oct 7, 2021
d266763
Improve docs for internal _F class
timj Oct 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/DM-29370.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add option to ignore NaNs in ``lsst.utils.tests.assertFloatsAlmostEqual``.
2 changes: 2 additions & 0 deletions doc/changes/DM-29701.api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``deprecate_pybind11`` now requires a ``version`` parameter.
This matches the upstream requirement from ``deprecated.deprecated``
1 change: 1 addition & 0 deletions doc/changes/DM-31141.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add test decorators to operate on cartesian product.
8 changes: 8 additions & 0 deletions doc/changes/DM-31722.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
* Several new packages added from ``pipe_base`` and ``daf_butler``:
* ``lsst.utils.timer``
* ``lsst.utils.classes``
* ``lsst.utils.introspection``
* ``lsst.utils.iteration``
* ``lsst.utils.logging``
* Added ``lsst.utils.doImportType`` to import a python type from a string and guaranteeing it is not a module.
* ``lsst.utils.get_caller_name`` is now deprecated in its current location and has been relocated to ``lsst.utils.introspection``.
20 changes: 20 additions & 0 deletions doc/lsst.utils/CHANGES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
lsst.utils v22.0 (2021-07-09)
=============================

Bug fix
-------

* Error reporting in `~lsst.utils.doImport` has been improved. [DM-27638]

lsst.utils v21.0 (2020-12-08)
=============================

New Features
------------

* Added a temporary directory context manager `lsst.utils.tests.temporaryDirectory`. [DM-26774]

API Change
----------

* Add an optional ``version`` parameter to `lsst.utils.deprecate_pybind11`. [DM-26285]
10 changes: 10 additions & 0 deletions doc/lsst.utils/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ Python API reference
.. automodapi:: lsst.utils.tests
:no-main-docstr:
:no-inherited-members:
.. automodapi:: lsst.utils.logging
:no-main-docstr:
.. automodapi:: lsst.utils.iteration
:no-main-docstr:
.. automodapi:: lsst.utils.classes
:no-main-docstr:
.. automodapi:: lsst.utils.introspection
:no-main-docstr:
.. automodapi:: lsst.utils.timer
:no-main-docstr:
135 changes: 135 additions & 0 deletions python/lsst/utils/classes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# This file is part of utils.
#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# Use of this source code is governed by a 3-clause BSD-style
# license that can be found in the LICENSE file.
#

"""Utilities to help with class creation.
"""

from __future__ import annotations

__all__ = ["Singleton", "cached_getter", "immutable"]

from typing import (
Any,
Callable,
Dict,
Type,
TypeVar,
)
import functools


class Singleton(type):
"""Metaclass to convert a class to a Singleton.

If this metaclass is used the constructor for the singleton class must
take no arguments. This is because a singleton class will only accept
the arguments the first time an instance is instantiated.
Therefore since you do not know if the constructor has been called yet it
is safer to always call it with no arguments and then call a method to
adjust state of the singleton.
"""

_instances: Dict[Type, Any] = {}

# Signature is intentionally not substitutable for type.__call__ (no *args,
# **kwargs) to require classes that use this metaclass to have no
# constructor arguments.
def __call__(cls) -> Any: # type: ignore
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__()
return cls._instances[cls]


_T = TypeVar("_T", bound="Type")


def immutable(cls: _T) -> _T:
"""Decorate a class to simulates a simple form of immutability.
timj marked this conversation as resolved.
Show resolved Hide resolved

A class decorated as `immutable` may only set each of its attributes once;
any attempts to set an already-set attribute will raise `AttributeError`.

Notes
-----
Subclasses of classes marked with ``@immutable`` are also immutable.

Because this behavior interferes with the default implementation for the
``pickle`` modules, `immutable` provides implementations of
``__getstate__`` and ``__setstate__`` that override this behavior.
Immutable classes can then implement pickle via ``__reduce__`` or
``__getnewargs__``.

Following the example of Python's built-in immutable types, such as `str`
and `tuple`, the `immutable` decorator provides a ``__copy__``
implementation that just returns ``self``, because there is no reason to
actually copy an object if none of its shared owners can modify it.

Similarly, objects that are recursively (i.e. are themselves immutable and
have only recursively immutable attributes) should also reimplement
``__deepcopy__`` to return ``self``. This is not done by the decorator, as
it has no way of checking for recursive immutability.
"""
def __setattr__(self: _T, name: str, value: Any) -> None: # noqa: N807
if hasattr(self, name):
raise AttributeError(f"{cls.__name__} instances are immutable.")
object.__setattr__(self, name, value)
# mypy says the variable here has signature (str, Any) i.e. no "self";
# I think it's just confused by descriptor stuff.
cls.__setattr__ = __setattr__ # type: ignore

def __getstate__(self: _T) -> dict: # noqa: N807
# Disable default state-setting when unpickled.
return {}
cls.__getstate__ = __getstate__

def __setstate__(self: _T, state: Any) -> None: # noqa: N807
# Disable default state-setting when copied.
# Sadly what works for pickle doesn't work for copy.
assert not state
cls.__setstate__ = __setstate__

def __copy__(self: _T) -> _T: # noqa: N807
return self
cls.__copy__ = __copy__
return cls


_S = TypeVar("_S")
_R = TypeVar("_R")


def cached_getter(func: Callable[[_S], _R]) -> Callable[[_S], _R]:
"""Decorate a method to caches the result.
timj marked this conversation as resolved.
Show resolved Hide resolved

Only works on methods that take only ``self``
as an argument, and returns the cached result on subsequent calls.

Notes
-----
This is intended primarily as a stopgap for Python 3.8's more sophisticated
``functools.cached_property``, but it is also explicitly compatible with
the `immutable` decorator, which may not be true of ``cached_property``.

`cached_getter` guarantees that the cached value will be stored in
an attribute named ``_cached_{name-of-decorated-function}``. Classes that
use `cached_getter` are responsible for guaranteeing that this name is not
otherwise used, and is included if ``__slots__`` is defined.
"""
attribute = f"_cached_{func.__name__}"

@functools.wraps(func)
def inner(self: _S) -> _R:
if not hasattr(self, attribute):
object.__setattr__(self, attribute, func(self))
return getattr(self, attribute)

return inner
32 changes: 31 additions & 1 deletion python/lsst/utils/doImport.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Use of this source code is governed by a 3-clause BSD-style
# license that can be found in the LICENSE file.

__all__ = ("doImport",)
__all__ = ("doImport", "doImportType")

import importlib
import types
Expand Down Expand Up @@ -81,3 +81,33 @@ def tryImport(module: str, fromlist: List[str],
infileComponents.insert(0, moduleComponents.pop())

raise ModuleNotFoundError(f"Unable to import {importable}")


def doImportType(importable: str) -> Type:
"""Import a python type given an importable string and return it.

Parameters
----------
importable : `str`
String containing dot-separated path of a Python class,
or member function.

Returns
-------
type : `type`
Type object. Can not return a module.

Raises
------
TypeError
``importable`` is not a `str` or the imported type is a module.
ModuleNotFoundError
No module in the supplied import string could be found.
ImportError
``importable`` is found but can not be imported or the requested
item could not be retrieved from the imported module.
"""
imported = doImport(importable)
if isinstance(imported, types.ModuleType):
raise TypeError(f"Import of {importable} returned a module and not a type.")
return imported
32 changes: 7 additions & 25 deletions python/lsst/utils/get_caller_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@

__all__ = ["get_caller_name"]

import inspect
from deprecated.sphinx import deprecated
from .introspection import get_caller_name as caller_name


@deprecated(reason="get_caller_name has moved to `lsst.utils.introspection.get_caller_name`."
" Will be removed in v26.",
version="v24", category=FutureWarning)
def get_caller_name(skip: int = 2) -> str:
"""Get the name of the caller method.

Expand All @@ -32,28 +36,6 @@ def get_caller_name(skip: int = 2) -> str:
name : `str`
Name of the caller as a string in the form ``module.class.method``.
An empty string is returned if ``skip`` exceeds the stack height.

Notes
-----
Adapted from from http://stackoverflow.com/a/9812105
by adding support to get the class from ``parentframe.f_locals['cls']``
"""
stack = inspect.stack()
start = 0 + skip
if len(stack) < start + 1:
return ''
parentframe = stack[start][0]

name = []
module = inspect.getmodule(parentframe)
if module:
name.append(module.__name__)
# add class name, if any
if 'self' in parentframe.f_locals:
name.append(type(parentframe.f_locals['self']).__name__)
elif 'cls' in parentframe.f_locals:
name.append(parentframe.f_locals['cls'].__name__)
codename = parentframe.f_code.co_name
if codename != '<module>': # top level usually
name.append(codename) # function or a method
return ".".join(name)
# Offset the stack level to account for redirect and deprecated wrapper.
return caller_name(stacklevel=skip + 2)