-
Notifications
You must be signed in to change notification settings - Fork 152
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
Add extended debug logging utils #158
Add extended debug logging utils #158
Conversation
7896cf3
to
b20c437
Compare
b20c437
to
1c82ed0
Compare
pass | ||
|
||
|
||
class _BaseHasExtendedDebugLogger(HasLogger[ExtendedDebugLogger]): # type: ignore |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noting this API of doing HasLogger[LoggerClass]
to create a new one that uses the new logger class is intentionally not documented so that we aren't committed to supporting this API.
# This class exists to a allow us to define the type of the logger. Once | ||
# python3.5 is deprecated this can be removed in favor of a simple type | ||
# annotation on the main class. | ||
logger = ExtendedDebugLogger("") # type: ExtendedDebugLogger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried a whole bunch of things to try and do this cleanly but this was the one way I could make it work without ugly side effects.
- Tried programatically populating
__annotations__
- Tried using a
typing.Generic
- Looked into
mypy
plugins - Looked into writing stub files.
1c82ed0
to
cf0c207
Compare
Hah, @lithp was also working on getting the logger available at the class level. Edit: oh, I see you already pinged him. Maybe you knew that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, so two primary things:
- Performance: no idea what the impact of adding a metaclass to the mix is. Maybe not good? Would we just not use this logger for py-evm and trinity?
- Getting it working on the class level almost certainly requires some magic. Brian took a look, and had another option. So it would be worth comparing the two incantations side by side for readability.
docs/utilities.rst
Outdated
... pass | ||
... | ||
>>> hasattr(MyClass, 'logger') | ||
True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you like, we could highlight the benefit here:
>>> MyClass.logger.debug('Can log from the class')
docs/utilities.rst
Outdated
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
|
||
Same as the ``HasLogger`` class except the logger it exposes is an instance of | ||
``ExtendedDebugLogger`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that if you don't run setup then this all still works, right? I guess it would be nice to specifically address that.
class NoisyClass(HasExtendedDebugLogger):
pass
NoisyClass.logger.debug2('What if I add a debug2 log without running setup_DEBUG2_logging?')
# replace it. | ||
return super().__new__(mcls, name, bases, namespace) | ||
if "__qualname__" not in namespace: | ||
raise AttributeError("Missing __qualname__") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've never really dived into meta classes. It all seems pretty magical to me still. Brian did another implementation that was also magical. Maybe he still has it floating around. I'm not sure which one was better. Also performance matters on some use cases, like py-evm/trinity. I don't know this code well enough to understand the performance impact.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that there should be no performance hit and this should more performant than the @property
based approach since metaclass code runs at class creation time which is module loading time. Assuming I am correct, there should be zero runtime overhead as everything should happen at module loading time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just scrounged through my reflog to find my solution:
class classproperty(Generic[TResult]):
def __init__(self, func: Callable[[Type[Any]], TResult]) -> None:
self.func = func
def __get__(self, instance: Any, owner: Type[Any]) -> TResult:
return self.func(owner)
def get_logger(cls: Type['BasePlugin']) -> logging.Logger:
return logging.getLogger(f'trinity.extensibility.plugin#{cls.__name__}')
class BasePlugin(ABC):
logger = classproperty(get_logger)
....
Where get_logger was pulled out to make mypy happy but could also be in-line with a # type: ignore
Never thought I'd say this but I expect that the metaclass is not only faster but also less magical.
@@ -35,6 +35,6 @@ extras=lint | |||
commands= | |||
flake8 {toxinidir}/eth_utils setup.py | |||
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs --warn-unused-ignore -p eth_utils | |||
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs tests/mypy_typing_of_functional_utils.py | |||
mypy --follow-imports=silent --ignore-missing-imports --disallow-incomplete-defs tests/type-checks/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Mirrored from my other comment. This was actually written to address the performance part since metaclass code runs at module loading time, effectively front-loading the work so that it's already done by the time you actually work with any of the classes. While metaclasses are magic, this I don't feel this is dark magic. I've tried to write reasonably exhaustive tests to ensure that it works as expected in all of the inheritance scenarios we may encounter, though now that I'm saying this, I realize I need to write a test that ensures this behaves nicely with classes that also inherit from |
Did not find a clean way to make this work nicely with |
9073987
to
ab256b1
Compare
Hm, I think it causes problems if you run tox locally and expect it to pick up your changes. |
I think the right thing to do is to drop the |
|
ab256b1
to
8994534
Compare
Dropped |
|
||
def __new__(mcls, name, bases, namespace): | ||
if "logger" in namespace: | ||
# If a logger was explicitely declared we shouldn't do anything to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/explicitely/explicitly/
eth_utils/logging.py
Outdated
logging.setLoggerClass(original_logger_cls) | ||
|
||
|
||
class HasLoggerMeta(ABCMeta): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this derive from ABCMeta
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because..... I don't know. removing.
Didn't look through this thoroughly but this approach seems reasonable! 👍 |
Replaces: ethereum/py-evm#1750
What was wrong?
We have a few libraries that need to log with more granularity than the default
logging
module provides. We also regularly want loggers attached to our classes and we end up re-inventing mechanisms to make this DRY across different codebases.How was it fixed?
Added the following utilities to
eth-utils
to make this easier.HasLogger
: inherit from this class and a logger is available on both the class and instances of the class at thelogger
propertyHasExtendedDebugLogger
: Same asHasLogger
but exposes anExtendedDebugLogger
ExtendedDebugLogger
: Subclass oflogging.Logger
that has adebug2(...)
logging method for logging at theDEBUG2
levelsetup_DEBUG2_logging
: Installs theDEBUG2
log level into the standard library logging module at the numeric level of 8 with the string name'DEBUG2'
.This improves on existing mechanisms by:
__qualname__
for the logger name from PEP3155 which seems to be exactly what we want.logger
property manually and respects that.HasExtendedDebugLogger
no longer relies on the user having calledsetup_DEBUG2_logging
to work. Calling the setup function is merely a nicety now that lets the logging module know the correct human readable name to use as well as makinglogging.DEBUG2
an importable property.Cute Animal Picture