Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
ymampaey please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Logger identity collisions and mutable scope resolution can break the promised isolation and hierarchy.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (5)
Use module-qualified identities for per-driver logger isolation · New Store scope at root initialization for all descendants · New Avoid ambient logging levels in class logger test · New Clarify propagate=False and separate VISA logger behavior · New Document permanent logging-registry growth caveat · New
What changed in this PR
Adds opt-in per-instrument logger hierarchies while preserving shared logging by default.
Changes:
- Adds configurable logger scope and hierarchical names.
- Applies scoped naming to VISA loggers.
- Adds tests, documentation, and a newsfragment.
| File | Description |
|---|---|
instrument_base.py |
Defines logger scopes and scoped-name generation. |
visa.py |
Applies scoped VISA logging. |
ip_to_visa.py |
Applies scoped logging to simulated VISA instruments. |
test_logger.py |
Tests scope, inheritance, filtering, and VISA behavior. |
logging_example.ipynb |
Documents scoped logger usage. |
8523.new |
Announces the feature. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| """ | ||
| root = self.root_instrument | ||
| if root.default_logger_scope == "instrument": | ||
| return ".".join((base, type(root).__name__, *self.name_parts)) |
There was a problem hiding this comment.
This would basically be equivalent to replacing base which is currently the name of this module qcodes.instrument.instrument_base with the name of the module that the instrument is defined in. qcodes.insrument.instrument_vendor.instrument_filename. I think I agreee that I would prefer this
There was a problem hiding this comment.
One blocker for replacing base with the name of the module the instrument is defined in: _logger_name is called with two different bases, name for self.log and VISA_LOGGER for self.visa_log. Drop base, and those two become the same logger name, so driver messages and wire traffic can no longer be separated at all. Module-only also wouldn't distinguish AMIModel430 from AMIModel4303D, since both live in AMI430_visa.py.
The problem is:
vendor_a.Model372 -> qcodes.instrument.instrument_base.Model372 ┐ same
vendor_b.Model372 -> qcodes.instrument.instrument_base.Model372 ┘ logger
Not sure if vendors will realistically have the same module number, but perhaps these other sources could cause a problem:
- Same driver, two sources: a driver in qcodes.instrument_drivers.X and a fork/variant in qcodes_contrib_drivers or a local copy. Same class name by construction, since one is derived from the other.
- Notebook drivers: a user's quick class MyDriver(VisaInstrument) in main, which collides with anyone else's MyDriver.
- Subclassing in place: class AMIModel430(AMIModel430) style local tweaks.
In the category "same lineage, different module". Copilot's finding is technically correct but low severity: it needs a self-inflicted name clash, and the consequence is a shared log level, not data loss or a crash.
We could do the following:
def _logger_name(self, base: str) -> str:
root = self.root_instrument
if root.default_logger_scope == "instrument":
- return ".".join((base, type(root).__name__, *self.name_parts))
+ return ".".join((base, full_class(root), *self.name_parts))
return base
So it would look like:
qcodes.instrument.instrument_base.qcodes.instrument_drivers.american_magnetics.AMI430_visa.AMIModel430.inst
qcodes.instrument.instrument_base.qcodes.instrument_drivers.tektronix.AWG5208.TektronixAWG5208.inst
└──────────────── full_class ────────────────┘
Which is safer but very verbose. Another alternative is leaving it as is and documenting the behavior.
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review. Note You can configure setup steps for Copilot code review separately from Copilot cloud agent with a |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #8523 +/- ##
==========================================
+ Coverage 72.00% 72.21% +0.21%
==========================================
Files 305 307 +2
Lines 32019 32333 +314
==========================================
+ Hits 23055 23350 +295
- Misses 8964 8983 +19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The permanent logging registry growth under the "instrument" scope was only described in the PR discussion, not in the user facing documentation. Add it to the caveats of the logging example notebook, together with the mitigation that instruments with stable names reuse their existing logger. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>



Closes #8522
Summary
Every instrument currently shares one
logging.Loggerobject, becauseInstrumentBase.__init__names it with a module constant:Since
logging.getLogger()caches by name,setLevel,addHandlerandpropagateset on one instrument apply to all of them.VisaInstrument.visa_loghas the same problem via the constantVISA_LOGGER.This PR lets a driver opt in to its own logger by setting one class attribute. QCoDeS only chooses the logger name — it still never sets levels, adds handlers or touches
propagate; that stays driver/application policy.The default is unchanged. Without opting in, logger names, records, formatting and filtering are byte-for-byte identical to today.
What the scoped names look like
The name is built from the driver class of the
root_instrumentfollowed by the instrument'sname_parts, and stays a descendant of the existing shared logger:Each node is a real ancestor of the next, so a level set at any level is inherited by everything below it, and a level configured on
qcodes.instrument.instrument_base(e.g. vialogger_levelsinqcodesrc.json) is still inherited by everything.Examples
Per instrument
Per driver class
This is the common driver-development case: a station often holds several instruments of the same driver (QCoDeS' own
AMI430_3Dtest fixture buildsmag_x,mag_yandmag_z, allAMIModel430) and you want DEBUG for all of them without listing their names.Because a level can be set on a logger before it exists, this also covers instruments created later and can be written declaratively in
qcodesrc.json:Submodules follow their instrument
Design notes
Why
name_partsrather thanfull_name.name_partsis QCoDeS' canonical identity for an instrument. Joining the parts with.rather than using the_-joinedfull_nameis what makes a channel logger a genuine child of its instrument's logger.Why the class comes from
root_instrument, nottype(self). Usingtype(self)would put an instrument and its own channel in different subtrees (....MyDriver.myinstnext to....DummyChannel.myinst_ChanA), soinst.log.logger.setLevel(DEBUG)would silently not reach the instrument's channels. Taking the class from the root instrument keeps the whole tree under one class node.Why the name extends the existing base instead of replacing it. A name derived from
type(self).__module__would put third-party drivers — and drivers defined in a notebook (__main__) — outside theqcodes.*tree, wherelogger_levelsanddictConfigrules scoped toqcodesno longer apply. Keepingbasealso preserves the separation betweenlogandvisa_log, which are created from different base names.default_logger_scopeis a plain class attribute, not aClassVar, matching the existingVisaInstrument.default_terminator/default_timeoutprecedent.Known caveat (documented)
Python's
loggingregistry keeps a strong reference to every logger and has no removal API, so under"instrument"scope repeatedly creating instruments with distinct names leaks one registry entry per name. Negligible for normal sessions; noted in the docs.Changes
src/qcodes/instrument/instrument_base.pyLoggerScope,default_logger_scope,_logger_name(), scopedself.logsrc/qcodes/instrument/visa.py,ip_to_visa.pyself.visa_logtests/test_logger.pydocs/examples/logging/logging_example.ipynbdocs/changes/newsfragments/Tests
New tests cover: default scope unchanged (regression guard); distinct loggers per instance;
setLevelisolation; inheritance from the shared logger; class-level DEBUG reaching instruments created afterwards and their submodules but not other drivers; per-instrument level overriding the class level; submodule logger being a child of its instrument;filter_instrumentstill working under scoping; and thevisa_logequivalents.Full suite passes (3249 passed, 270 skipped; the one failure in
test_installation_infois a pre-existing local environment issue unrelated to this change and reproduces on unmodifiedmain).pyrightreports 0 errors andpre-commit run --allpasses. The updated notebook was executed end to end.