From 47a65ad2cd70319e5c3c933be82d31e111c69126 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 15:15:08 +0000 Subject: [PATCH] chore: normalise all tracked text files to LF + enforce via .gitattributes 46 tracked text files had drifted to CRLF endings despite the repo's LF rule (surfaced by the 2026-07-25 hygiene crlf tier). Strip the carriage returns in one mechanical pass and add '* text=auto eol=lf' so git normalises every text file at checkin/checkout from now on, subsuming the previous targeted shebang/script rules. Verified: 'git diff --ignore-cr-at-eol' shows zero non-ending content change, no CRLF text files remain, and the full test suite passes post-conversion. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PzN9PZfG5zXMVku8ckSqkC --- .gitattributes | 7 +- .gitignore | 228 +++--- CODE_OF_CONDUCT.md | 612 +++++++------- LICENSE | 42 +- MANIFEST.in | 22 +- autonerves/__init__.py | 210 ++--- autonerves/class_path.py | 122 +-- autonerves/conf.py | 772 +++++++++--------- autonerves/directory_config.py | 486 +++++------ autonerves/exc.py | 12 +- autonerves/fitsable.py | 484 +++++------ autonerves/json_prior/config.py | 488 +++++------ autonerves/mock/mock_real.py | 122 +-- autonerves/output.py | 128 +-- autonerves/tools/decorators.py | 160 ++-- pyproject.toml | 130 +-- setup.py | 14 +- test_autonerves/conftest.py | 52 +- test_autonerves/files/config/embedded.yaml | 8 +- test_autonerves/files/config/general.yaml | 2 +- test_autonerves/files/config/label.ini | 16 +- test_autonerves/files/config/logging.yaml | 24 +- test_autonerves/files/config/one/two.ini | 4 +- test_autonerves/files/config/output.yaml | 4 +- .../files/config/priors/mock_real.json | 16 +- .../config/priors/subdirectory/subconfig.yaml | 8 +- .../files/config/priors/test_yaml_config.yaml | 8 +- test_autonerves/files/config/text/label.ini | 6 +- .../files/config_flip/general.yaml | 2 +- .../files/default/default/other.ini | 2 +- .../files/default/default_file.ini | 2 +- test_autonerves/files/default/embedded.yaml | 6 +- test_autonerves/files/default/general.ini | 10 +- test_autonerves/files/default/logging.yaml | 24 +- test_autonerves/files/default/one.yaml | 6 +- .../files/default/priors/mock_real.json | 26 +- test_autonerves/files/default/text/label.ini | 8 +- .../json_prior/source_code/module.py | 8 +- .../source_code/subdirectory/subconfig.py | 6 +- .../json_prior/test_json_config.py | 206 ++--- .../json_prior/test_yaml_config.py | 54 +- test_autonerves/test_config.py | 112 +-- test_autonerves/test_decorator.py | 38 +- test_autonerves/test_default.py | 176 ++-- test_autonerves/test_fitsable.py | 158 ++-- test_autonerves/test_output_config.py | 92 +-- test_autonerves/tools/test_decorators.py | 34 +- 47 files changed, 2580 insertions(+), 2577 deletions(-) diff --git a/.gitattributes b/.gitattributes index aa567a0..2af6fbd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ -# Executable scripts must keep LF endings — a CRLF shebang breaks execution on HPC/Linux. -scripts/*.py eol=lf +# All files use Unix line endings (LF) — see AGENTS.md. text=auto lets git +# detect binaries; eol=lf normalises every text file on checkout and checkin, +# subsuming the earlier targeted shebang/script rules. Tree-wide CRLF +# normalisation landed alongside this file (2026-07-25). +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index 17fbd68..a4e5c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,114 +1,114 @@ -test_autonerves/unit/json_prior/code/priors/ - -test/autofit/test_fit -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test_autoarray / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.idea -workspace/output/ -output -test/optimize/test_fit -test/test_files/text/ -test/ -.DS_Store +test_autonerves/unit/json_prior/code/priors/ + +test/autofit/test_fit +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test_autoarray / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.idea +workspace/output/ +output +test/optimize/test_fit +test/test_files/text/ +test/ +.DS_Store diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 776848e..caa083c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,307 +1,307 @@ -# PyAutoNerves Code of Conduct - -**Table of Contents** - -- [The Short Version](#the-short-version) -- [The Longer Version](#the-longer-version) - - [PyAutoNerves Diversity Statement](#project-diversity-statement) - - [PyAutoNerves Code of Conduct: Introduction & Scope](#project-code-of-conduct-introduction--scope) - - [Standards for Behavior](#standards-for-behavior) - - [Unacceptable Behavior](#unacceptable-behavior) - - [Reporting Guidelines](#reporting-guidelines) - - [How to Submit a Report](#how-to-submit-a-report) - - [Person(s) Responsible for Resolving Complaints](#persons-responsible-for-resolving-complaints) - - [Conflicts of Interest](#conflicts-of-interest) - - [What to Include in a Report](#what-to-include-in-a-report) - - [Enforcement: What Happens After a Report is Filed?](#enforcement-what-happens-after-a-report-is-filed) - - [Acknowledgment and Responding to Immediate Needs](#acknowledgment-and-responding-to-immediate-needs) - - [Reviewing the Report](#reviewing-the-report) - - [Contacting the Person Reported](#contacting-the-person-reported) - - [Response and Potential Consequences](#response-and-potential-consequences) - - [Appealing a Decision](#appealing-a-decision) - - [Timeline Summary:](#timeline-summary) - - [Confirming Receipt](#confirming-receipt) - - [Reviewing the Report](#reviewing-the-report-1) - - [Consequences & Resolution](#consequences--resolution) -- [License](#license) - -## The Short Version - -Be kind to others. Do not insult or put down others. Behave professionally. Remember that harassment and sexist, -racist, or exclusionary jokes are not appropriate for PyAutoNerves. - -All communication should be appropriate for a professional audience including people of many different backgrounds. -Sexual language and imagery is not appropriate. - -PyAutoNerves is dedicated to providing a harassment-free community for everyone, regardless of gender, sexual orientation, -gender identity and expression, disability, physical appearance, body size, race, or religion. We do not tolerate -harassment of community members in any form. - -Thank you for helping make this a welcoming, friendly community for all. - -## The Longer Version - -### PyAutoNerves Diversity Statement - -PyAutoNerves welcomes and encourages participation in our community by people of all backgrounds and identities. We -are committed to promoting and sustaining a culture that values mutual respect, tolerance, and learning, and we work -together as a community to help each other live out these values. - -We have created this diversity statement because we believe that a diverse community is stronger, more vibrant, -and produces better software and better science. A diverse community where people treat each other with respect has -more potential contributors, more sources for ideas, and fewer shared assumptions that might hinder development -or research. - -Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognize that there -are specific identities that are impacted by systemic discrimination and marginalization. We welcome all people to -participate in the PyAutoNerves community regardless of their identity or background. - -### PyAutoNerves Code of Conduct: Introduction & Scope - -This code of conduct should be honored by everyone who participates in the PyAutoNerves community. It should be -honored in any PyAutoNerves-related activities, by anyone claiming affiliation with PyAutoNerves, and especially when -someone is representing PyAutoNerves in any role (including as an event volunteer or speaker). - -This code of conduct applies to all spaces managed by PyAutoNerves, including all public and private mailing lists, -issue trackers, wikis, forums, and any other communication channel used by our community. The code of conduct equally -applies at PyAutoNerves events and governs standards of behavior for attendees, speakers, volunteers, booth staff, -and event sponsors. - -This code is not exhaustive or complete. It serves to distill our understanding of a collaborative, inclusive -community culture. Please try to follow this code in spirit as much as in letter, to create a friendly and -productive environment that enriches the PyAutoNerves community. - -The PyAutoNerves Code of Conduct follows below. - -### Standards for Behavior - -PyAutoNerves is a worldwide community. All communication should be appropriate for a professional audience including -people of many different backgrounds. - -**Please always be kind and courteous. There's never a need to be mean or rude or disrespectful.** Thank you for -helping make this a welcoming, friendly community for all. - -We strive to: - -**Be empathetic, welcoming, friendly, and patient.** We remember that PyAutoNerves is crafted by human beings who -deserve to be treated with kindness and empathy. We work together to resolve conflict and assume good intentions. -We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal -attack. A community where people feel uncomfortable or threatened is not a productive one. - -**Be collaborative.** Our work depends on the participation of many people, and in turn others depend on our work. -Open source communities depend on effective and friendly collaboration to achieve their goals. - -**Be inquisitive.** Nobody knows everything! Asking questions early avoids many problems later, so we encourage -questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. - -**Be careful in the words that we choose.** We are careful and respectful in our communication and we take -responsibility for our own speech. Be kind to others. Do not insult or put down other members of the community. - -#### Unacceptable Behavior - -We are committed to making participation in this community a harassment-free experience. - -We will not accept harassment or other exclusionary behaviours, such as: - -- The use of sexualized language or imagery -- Excessive profanity (please avoid curse words; people differ greatly in their sensitivity to swearing) -- Posting sexually explicit or violent material -- Violent or intimidating threats or language directed against another person -- Inappropriate physical contact and/or unwelcome sexual attention or sexual comments -- Sexist, racist, or otherwise discriminatory jokes and language -- Trolling or insulting and derogatory comments -- Written or verbal comments which have the effect of excluding people on the basis of membership in a specific group, -including level of experience, gender, gender identity and expression, sexual orientation, disability, neurotype, -personal appearance, body size, race, ethnicity, age, religion, or nationality -- Public or private harassment -- Sharing private content, such as emails sent privately or non-publicly, or direct message history, without the -sender's consent -- Continuing to initiate interaction (such as photography, recording, messaging, or conversation) with someone after -being asked to stop -- Sustained disruption of talks, events, or communications, such as heckling of a speaker -- Publishing (or threatening to post) other people's personally identifying information ("doxing"), such as -physical or electronic addresses, without explicit permission -- Other unethical or unprofessional conduct -- Advocating for, or encouraging, any of the above behaviors - -### Reporting Guidelines - -If you believe someone is violating the code of conduct, please report this in a timely manner. Code of conduct -violations reduce the value of the community for everyone. The PyAutoNerves leadership team takes reports of misconduct -very seriously and is committed to preserving and maintaining the welcoming nature of our community. - -**All reports will be kept confidential.** - -In some cases we may determine that a public statement will need to be made. If that's the case, the identities of -all involved parties and reporters will remain confidential unless those individuals instruct us otherwise. - -All complaints will be reviewed and investigated and will result in a response that is deemed necessary and -appropriate to the circumstances. The PyAutoNerves team commits to maintaining confidentiality with regard to the -reporter of an incident. - -For possibly unintentional breaches of the code of conduct, you may want to respond to the person and point out -this code of conduct (either in public or in private, whatever is most appropriate). If you would prefer not to do -that, please report the issue to PyAutoNerves directly, or ask James Nightingale for advice in confidence. Complete contact -information is below, under "How to Submit a Report." - -Take care of each other. Alert PyAutoNerves if you notice a dangerous situation, someone in distress, or violations of -this code of conduct, even if they seem inconsequential. - -#### How to Submit a Report - -**If you feel your safety is in jeopardy or the situation is an emergency, we urge you to contact local law enforcement before making a report to PyAutoNerves.** (In the U.K., dial 999.) - -PyAutoNerves is committed to promptly addressing any reported issues. If you have experienced or witnessed behavior that -violates the PyAutoNerves Code of Conduct, please report it by sending an email to one of the members of the PyAutoNerves -CoC Enforcement Team. - -#### Person(s) Responsible for Resolving Complaints - -All reports of breaches of the code of conduct will be investigated and handled by the **PyAutoNerves Code of Conduct Enforcement Team**. - -The current PyAutoNerves Code of Conduct Enforcement Team consists of: - -- James Nightingale - - - [*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) - -#### Conflicts of Interest - -In the event of any conflict of interest, the team member will immediately notify the PyAutoNerves Code of Conduct -Enforcement Team and recuse themselves if necessary. - -#### What to Include in a Report - -Our ability to address any code of conduct breaches in a timely and effective manner is impacted by the amount of -information you can provide, so, **our reporting form asks you to include as much of the following information as you can**: - -- **Your contact info** (so we can get in touch with you if we need to follow up). This will be kept confidential. -If you wish to remain anonymous, your information will not be shared beyond the person receiving the initial report. -- The **approximate time and location of the incident** (please be as specific as possible) -- **Identifying information** (e.g. name, nickname, screen name, physical description) of the individual whose -behavior is being reported -- **Description of the behavior** (if reporting harassing language, please be specific about the words -used), **your account of what happened**, and any available **supporting records** (e.g. email, GitHub issue, screenshots, etc.) -- **Description of the circumstances/context** surrounding the incident -- Let us know **if the incident is ongoing**, and/or if this is part of an ongoing pattern of behavior -- Names and contact info, if possible, of **anyone else who witnessed** or was involved in this incident. (Did -anyone else observe the incident?) -- **Any other relevant information** you believe we should have - -At PyAutoNerves Events: Event staff will attempt to gather and write down the above information from anyone making a -verbal report in-person at an event. Recording the details in writing is exceedingly important in order for us to -effectively respond to reports. If event staff write down a report taken verbally, then the person making the -report will be asked to review the written report for accuracy. - -**If urgent action is needed regarding an incident at an in-person event, we strongly encourage you to reach out to the local event staff for immediate assistance.** - -### Enforcement: What Happens After a Report is Filed? - -What happens after a report is filed? - -#### Acknowledgment and Responding to Immediate Needs - -PyAutoNerves and/or our event staff will attempt to ensure your safety and help with any immediate needs, particularly -at an in-person event. PyAutoNerves will make every effort to **acknowledge receipt within 24 hours** (and we'll aim -for much more quickly than that). - - - -#### Reviewing the Report - -PyAutoNerves will make all efforts to **review the incident within three days** and determine: - -- Whether this is an ongoing situation, or if there is a threat to anyone's physical safety -- What happened -- Whether this event constitutes a code of conduct violation -- Who the bad actor was, if any - -#### Contacting the Person Reported - -After PyAutoNerves has had time to review and discuss the report, someone will attempt to contact the person who is the -subject of the report to inform them of what has been reported about them. We will then ask that person for their -account of what happened. - -#### Response and Potential Consequences - -Once PyAutoNerves has completed our investigation of the report, we will make a decision as to how to respond. The -person making a report will not normally be consulted as to the proposed resolution of the issue, except insofar as -we need to understand how to help them feel safe. - -Potential consequences for violating the PyAutoNerves code of conduct include: - -- Nothing (if we determine that no violation occurred) -- Private feedback or reprimand from PyAutoNerves to the individual(s) involved -- Warning the person to cease their behavior and that any further reports will result in sanctions -- A public announcement that an incident occurred -- Mediation (only if both reporter and reportee agree) -- An imposed vacation (e.g. asking someone to "take a week off" from a mailing list) -- A permanent or temporary ban from some or all PyAutoNerves spaces (mailing lists, GitHub repos, in-person events, etc.) -- Assistance to the complainant with a report to other bodies, for example, institutional offices or appropriate law enforcement agencies -- Removing a person from PyAutoNerves membership or other formal affiliation -- Publishing an account of the harassment and calling for the resignation of the alleged harasser from their -responsibilities (usually pursued by people without formal authority: may be called for if the person is the -event leader, or refuses to stand aside from the conflict of interest, or similar) -- Any other response that PyAutoNerves deems necessary and appropriate to the situation - -At PyAutoNerves events, if a participant engages in behavior that violates this code of conduct, the conference -organizers and staff may take any action they deem appropriate. - -Potential consequences for violating the PyAutoNerves Code of Conduct at an in-person event include: - -- Warning the person to cease their behavior and that any further reports will result in sanctions -- Requiring that the person avoid any interaction with, and physical proximity to, the person they are harassing -for the remainder of the event -- Ending a talk that violates the policy early -- Not publishing the video or slides of a talk that violated the policy -- Not allowing a speaker who violated the policy to give (further) talks at the event now or in the future -- Immediately ending any event volunteer responsibilities and privileges the reported person holds -- Requiring that the person not volunteer for future events PyAutoNerves runs (either indefinitely or for a certain time period) -- Expelling the person from the event without a refund -- Requiring that the person immediately leave the event and not return -- Banning the person from future events (either indefinitely or for a certain time period) -- Any other response that PyAutoNerves deems necessary and appropriate to the situation - -No one espousing views or values contrary to the standards of our code of conduct will be permitted to hold any -position representing PyAutoNerves, including volunteer positions. PyAutoNerves has the right and responsibility to -remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not -aligned with this code of conduct. - -We aim to **respond within one week** to the original reporter with either a resolution or an explanation of why the -situation is not yet resolved. - -We will contact the person who is the subject of the report to let them know what actions will be taken as a result of -the report, if any. - -Our policy is to make sure that everyone aware of the initial incident is also made aware that official action has -been taken, while still respecting the privacy of individuals. PyAutoNerves may choose to make a public report of the -incident, while maintaining the anonymity of those involved. - -#### Appealing a Decision - -To appeal a decision of PyAutoNerves, contact James Nightingale via email at -[*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) with your appeal and -the Leadership Team will review the case. - -### Timeline Summary: - -#### Confirming Receipt - -PyAutoNerves will make every effort to acknowledge receipt of a report **within 24 hours** (and we'll aim for much more -quickly than that). - -#### Reviewing the Report - -PyAutoNerves will make all efforts to review the incident **within three days**. - -#### Consequences & Resolution - -We aim to respond **within one week** to the original reporter with either a resolution or an explanation of why -the situation is not yet resolved. - -## License - -This code of conduct has been adapted from [*NUMFOCUS code of conduct*](https://numfocus.org/code-of-conduct), -which is adapted from numerous sources, including the [*Geek Feminism wiki, created by the Ada Initiative and other volunteers, which is under a Creative Commons Zero license*](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy), the [*Contributor Covenant version 1.2.0*](http://contributor-covenant.org/version/1/2/0/), the [*Bokeh Code of Conduct*](https://github.com/bokeh/bokeh/blob/main/docs/CODE_OF_CONDUCT.md), the [*SciPy Code of Conduct*](https://github.com/jupyter/governance/blob/main/conduct/enforcement.md), the [*Carpentries Code of Conduct*](https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html#enforcement-manual), and the [*NeurIPS Code of Conduct*](https://neurips.cc/public/CodeOfConduct). - +# PyAutoNerves Code of Conduct + +**Table of Contents** + +- [The Short Version](#the-short-version) +- [The Longer Version](#the-longer-version) + - [PyAutoNerves Diversity Statement](#project-diversity-statement) + - [PyAutoNerves Code of Conduct: Introduction & Scope](#project-code-of-conduct-introduction--scope) + - [Standards for Behavior](#standards-for-behavior) + - [Unacceptable Behavior](#unacceptable-behavior) + - [Reporting Guidelines](#reporting-guidelines) + - [How to Submit a Report](#how-to-submit-a-report) + - [Person(s) Responsible for Resolving Complaints](#persons-responsible-for-resolving-complaints) + - [Conflicts of Interest](#conflicts-of-interest) + - [What to Include in a Report](#what-to-include-in-a-report) + - [Enforcement: What Happens After a Report is Filed?](#enforcement-what-happens-after-a-report-is-filed) + - [Acknowledgment and Responding to Immediate Needs](#acknowledgment-and-responding-to-immediate-needs) + - [Reviewing the Report](#reviewing-the-report) + - [Contacting the Person Reported](#contacting-the-person-reported) + - [Response and Potential Consequences](#response-and-potential-consequences) + - [Appealing a Decision](#appealing-a-decision) + - [Timeline Summary:](#timeline-summary) + - [Confirming Receipt](#confirming-receipt) + - [Reviewing the Report](#reviewing-the-report-1) + - [Consequences & Resolution](#consequences--resolution) +- [License](#license) + +## The Short Version + +Be kind to others. Do not insult or put down others. Behave professionally. Remember that harassment and sexist, +racist, or exclusionary jokes are not appropriate for PyAutoNerves. + +All communication should be appropriate for a professional audience including people of many different backgrounds. +Sexual language and imagery is not appropriate. + +PyAutoNerves is dedicated to providing a harassment-free community for everyone, regardless of gender, sexual orientation, +gender identity and expression, disability, physical appearance, body size, race, or religion. We do not tolerate +harassment of community members in any form. + +Thank you for helping make this a welcoming, friendly community for all. + +## The Longer Version + +### PyAutoNerves Diversity Statement + +PyAutoNerves welcomes and encourages participation in our community by people of all backgrounds and identities. We +are committed to promoting and sustaining a culture that values mutual respect, tolerance, and learning, and we work +together as a community to help each other live out these values. + +We have created this diversity statement because we believe that a diverse community is stronger, more vibrant, +and produces better software and better science. A diverse community where people treat each other with respect has +more potential contributors, more sources for ideas, and fewer shared assumptions that might hinder development +or research. + +Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognize that there +are specific identities that are impacted by systemic discrimination and marginalization. We welcome all people to +participate in the PyAutoNerves community regardless of their identity or background. + +### PyAutoNerves Code of Conduct: Introduction & Scope + +This code of conduct should be honored by everyone who participates in the PyAutoNerves community. It should be +honored in any PyAutoNerves-related activities, by anyone claiming affiliation with PyAutoNerves, and especially when +someone is representing PyAutoNerves in any role (including as an event volunteer or speaker). + +This code of conduct applies to all spaces managed by PyAutoNerves, including all public and private mailing lists, +issue trackers, wikis, forums, and any other communication channel used by our community. The code of conduct equally +applies at PyAutoNerves events and governs standards of behavior for attendees, speakers, volunteers, booth staff, +and event sponsors. + +This code is not exhaustive or complete. It serves to distill our understanding of a collaborative, inclusive +community culture. Please try to follow this code in spirit as much as in letter, to create a friendly and +productive environment that enriches the PyAutoNerves community. + +The PyAutoNerves Code of Conduct follows below. + +### Standards for Behavior + +PyAutoNerves is a worldwide community. All communication should be appropriate for a professional audience including +people of many different backgrounds. + +**Please always be kind and courteous. There's never a need to be mean or rude or disrespectful.** Thank you for +helping make this a welcoming, friendly community for all. + +We strive to: + +**Be empathetic, welcoming, friendly, and patient.** We remember that PyAutoNerves is crafted by human beings who +deserve to be treated with kindness and empathy. We work together to resolve conflict and assume good intentions. +We may all experience some frustration from time to time, but we do not allow frustration to turn into a personal +attack. A community where people feel uncomfortable or threatened is not a productive one. + +**Be collaborative.** Our work depends on the participation of many people, and in turn others depend on our work. +Open source communities depend on effective and friendly collaboration to achieve their goals. + +**Be inquisitive.** Nobody knows everything! Asking questions early avoids many problems later, so we encourage +questions, although we may direct them to the appropriate forum. We will try hard to be responsive and helpful. + +**Be careful in the words that we choose.** We are careful and respectful in our communication and we take +responsibility for our own speech. Be kind to others. Do not insult or put down other members of the community. + +#### Unacceptable Behavior + +We are committed to making participation in this community a harassment-free experience. + +We will not accept harassment or other exclusionary behaviours, such as: + +- The use of sexualized language or imagery +- Excessive profanity (please avoid curse words; people differ greatly in their sensitivity to swearing) +- Posting sexually explicit or violent material +- Violent or intimidating threats or language directed against another person +- Inappropriate physical contact and/or unwelcome sexual attention or sexual comments +- Sexist, racist, or otherwise discriminatory jokes and language +- Trolling or insulting and derogatory comments +- Written or verbal comments which have the effect of excluding people on the basis of membership in a specific group, +including level of experience, gender, gender identity and expression, sexual orientation, disability, neurotype, +personal appearance, body size, race, ethnicity, age, religion, or nationality +- Public or private harassment +- Sharing private content, such as emails sent privately or non-publicly, or direct message history, without the +sender's consent +- Continuing to initiate interaction (such as photography, recording, messaging, or conversation) with someone after +being asked to stop +- Sustained disruption of talks, events, or communications, such as heckling of a speaker +- Publishing (or threatening to post) other people's personally identifying information ("doxing"), such as +physical or electronic addresses, without explicit permission +- Other unethical or unprofessional conduct +- Advocating for, or encouraging, any of the above behaviors + +### Reporting Guidelines + +If you believe someone is violating the code of conduct, please report this in a timely manner. Code of conduct +violations reduce the value of the community for everyone. The PyAutoNerves leadership team takes reports of misconduct +very seriously and is committed to preserving and maintaining the welcoming nature of our community. + +**All reports will be kept confidential.** + +In some cases we may determine that a public statement will need to be made. If that's the case, the identities of +all involved parties and reporters will remain confidential unless those individuals instruct us otherwise. + +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The PyAutoNerves team commits to maintaining confidentiality with regard to the +reporter of an incident. + +For possibly unintentional breaches of the code of conduct, you may want to respond to the person and point out +this code of conduct (either in public or in private, whatever is most appropriate). If you would prefer not to do +that, please report the issue to PyAutoNerves directly, or ask James Nightingale for advice in confidence. Complete contact +information is below, under "How to Submit a Report." + +Take care of each other. Alert PyAutoNerves if you notice a dangerous situation, someone in distress, or violations of +this code of conduct, even if they seem inconsequential. + +#### How to Submit a Report + +**If you feel your safety is in jeopardy or the situation is an emergency, we urge you to contact local law enforcement before making a report to PyAutoNerves.** (In the U.K., dial 999.) + +PyAutoNerves is committed to promptly addressing any reported issues. If you have experienced or witnessed behavior that +violates the PyAutoNerves Code of Conduct, please report it by sending an email to one of the members of the PyAutoNerves +CoC Enforcement Team. + +#### Person(s) Responsible for Resolving Complaints + +All reports of breaches of the code of conduct will be investigated and handled by the **PyAutoNerves Code of Conduct Enforcement Team**. + +The current PyAutoNerves Code of Conduct Enforcement Team consists of: + +- James Nightingale + + - [*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) + +#### Conflicts of Interest + +In the event of any conflict of interest, the team member will immediately notify the PyAutoNerves Code of Conduct +Enforcement Team and recuse themselves if necessary. + +#### What to Include in a Report + +Our ability to address any code of conduct breaches in a timely and effective manner is impacted by the amount of +information you can provide, so, **our reporting form asks you to include as much of the following information as you can**: + +- **Your contact info** (so we can get in touch with you if we need to follow up). This will be kept confidential. +If you wish to remain anonymous, your information will not be shared beyond the person receiving the initial report. +- The **approximate time and location of the incident** (please be as specific as possible) +- **Identifying information** (e.g. name, nickname, screen name, physical description) of the individual whose +behavior is being reported +- **Description of the behavior** (if reporting harassing language, please be specific about the words +used), **your account of what happened**, and any available **supporting records** (e.g. email, GitHub issue, screenshots, etc.) +- **Description of the circumstances/context** surrounding the incident +- Let us know **if the incident is ongoing**, and/or if this is part of an ongoing pattern of behavior +- Names and contact info, if possible, of **anyone else who witnessed** or was involved in this incident. (Did +anyone else observe the incident?) +- **Any other relevant information** you believe we should have + +At PyAutoNerves Events: Event staff will attempt to gather and write down the above information from anyone making a +verbal report in-person at an event. Recording the details in writing is exceedingly important in order for us to +effectively respond to reports. If event staff write down a report taken verbally, then the person making the +report will be asked to review the written report for accuracy. + +**If urgent action is needed regarding an incident at an in-person event, we strongly encourage you to reach out to the local event staff for immediate assistance.** + +### Enforcement: What Happens After a Report is Filed? + +What happens after a report is filed? + +#### Acknowledgment and Responding to Immediate Needs + +PyAutoNerves and/or our event staff will attempt to ensure your safety and help with any immediate needs, particularly +at an in-person event. PyAutoNerves will make every effort to **acknowledge receipt within 24 hours** (and we'll aim +for much more quickly than that). + + + +#### Reviewing the Report + +PyAutoNerves will make all efforts to **review the incident within three days** and determine: + +- Whether this is an ongoing situation, or if there is a threat to anyone's physical safety +- What happened +- Whether this event constitutes a code of conduct violation +- Who the bad actor was, if any + +#### Contacting the Person Reported + +After PyAutoNerves has had time to review and discuss the report, someone will attempt to contact the person who is the +subject of the report to inform them of what has been reported about them. We will then ask that person for their +account of what happened. + +#### Response and Potential Consequences + +Once PyAutoNerves has completed our investigation of the report, we will make a decision as to how to respond. The +person making a report will not normally be consulted as to the proposed resolution of the issue, except insofar as +we need to understand how to help them feel safe. + +Potential consequences for violating the PyAutoNerves code of conduct include: + +- Nothing (if we determine that no violation occurred) +- Private feedback or reprimand from PyAutoNerves to the individual(s) involved +- Warning the person to cease their behavior and that any further reports will result in sanctions +- A public announcement that an incident occurred +- Mediation (only if both reporter and reportee agree) +- An imposed vacation (e.g. asking someone to "take a week off" from a mailing list) +- A permanent or temporary ban from some or all PyAutoNerves spaces (mailing lists, GitHub repos, in-person events, etc.) +- Assistance to the complainant with a report to other bodies, for example, institutional offices or appropriate law enforcement agencies +- Removing a person from PyAutoNerves membership or other formal affiliation +- Publishing an account of the harassment and calling for the resignation of the alleged harasser from their +responsibilities (usually pursued by people without formal authority: may be called for if the person is the +event leader, or refuses to stand aside from the conflict of interest, or similar) +- Any other response that PyAutoNerves deems necessary and appropriate to the situation + +At PyAutoNerves events, if a participant engages in behavior that violates this code of conduct, the conference +organizers and staff may take any action they deem appropriate. + +Potential consequences for violating the PyAutoNerves Code of Conduct at an in-person event include: + +- Warning the person to cease their behavior and that any further reports will result in sanctions +- Requiring that the person avoid any interaction with, and physical proximity to, the person they are harassing +for the remainder of the event +- Ending a talk that violates the policy early +- Not publishing the video or slides of a talk that violated the policy +- Not allowing a speaker who violated the policy to give (further) talks at the event now or in the future +- Immediately ending any event volunteer responsibilities and privileges the reported person holds +- Requiring that the person not volunteer for future events PyAutoNerves runs (either indefinitely or for a certain time period) +- Expelling the person from the event without a refund +- Requiring that the person immediately leave the event and not return +- Banning the person from future events (either indefinitely or for a certain time period) +- Any other response that PyAutoNerves deems necessary and appropriate to the situation + +No one espousing views or values contrary to the standards of our code of conduct will be permitted to hold any +position representing PyAutoNerves, including volunteer positions. PyAutoNerves has the right and responsibility to +remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not +aligned with this code of conduct. + +We aim to **respond within one week** to the original reporter with either a resolution or an explanation of why the +situation is not yet resolved. + +We will contact the person who is the subject of the report to let them know what actions will be taken as a result of +the report, if any. + +Our policy is to make sure that everyone aware of the initial incident is also made aware that official action has +been taken, while still respecting the privacy of individuals. PyAutoNerves may choose to make a public report of the +incident, while maintaining the anonymity of those involved. + +#### Appealing a Decision + +To appeal a decision of PyAutoNerves, contact James Nightingale via email at +[*james.w.nightingale@durham.ac.uk*](mailto:james.w.nightingale@durham.ac.uk) with your appeal and +the Leadership Team will review the case. + +### Timeline Summary: + +#### Confirming Receipt + +PyAutoNerves will make every effort to acknowledge receipt of a report **within 24 hours** (and we'll aim for much more +quickly than that). + +#### Reviewing the Report + +PyAutoNerves will make all efforts to review the incident **within three days**. + +#### Consequences & Resolution + +We aim to respond **within one week** to the original reporter with either a resolution or an explanation of why +the situation is not yet resolved. + +## License + +This code of conduct has been adapted from [*NUMFOCUS code of conduct*](https://numfocus.org/code-of-conduct), +which is adapted from numerous sources, including the [*Geek Feminism wiki, created by the Ada Initiative and other volunteers, which is under a Creative Commons Zero license*](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy), the [*Contributor Covenant version 1.2.0*](http://contributor-covenant.org/version/1/2/0/), the [*Bokeh Code of Conduct*](https://github.com/bokeh/bokeh/blob/main/docs/CODE_OF_CONDUCT.md), the [*SciPy Code of Conduct*](https://github.com/jupyter/governance/blob/main/conduct/enforcement.md), the [*Carpentries Code of Conduct*](https://docs.carpentries.org/topic_folders/policies/code-of-conduct.html#enforcement-manual), and the [*NeurIPS Code of Conduct*](https://neurips.cc/public/CodeOfConduct). + **PyAutoNerves Code of Conduct is licensed under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/).** \ No newline at end of file diff --git a/LICENSE b/LICENSE index 3b62036..cee9a8f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 Richard Hayes - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2018 Richard Hayes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 9b76746..7368c01 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,12 @@ -# MANIFEST.in -exclude .gitignore -include LICENSE -include README.md -prune .git -prune build -prune dist -recursive-exclude *.egg-info * -include requirements.txt - -global-exclude test_autonerves +# MANIFEST.in +exclude .gitignore +include LICENSE +include README.md +prune .git +prune build +prune dist +recursive-exclude *.egg-info * +include requirements.txt + +global-exclude test_autonerves recursive-exclude test_autonerves * \ No newline at end of file diff --git a/autonerves/__init__.py b/autonerves/__init__.py index 2796b3e..23fce7c 100644 --- a/autonerves/__init__.py +++ b/autonerves/__init__.py @@ -1,105 +1,105 @@ -""" -autonerves — configuration, serialization, and I/O helpers for the PyAuto ecosystem. - -Text-format I/O surfaces: - -- :mod:`autonerves.dictable` — JSON (``output_to_json`` / ``from_json``) -- :mod:`autonerves.fitsable` — FITS (``output_to_fits`` / ``ndarray_via_fits_from``) -- :mod:`autonerves.csvable` — CSV (``output_to_csv`` / ``list_from_csv``) -""" -import sys -import warnings -from pathlib import Path - - -def _python_version_check_bypassed(): - """ - Return True iff the user's workspace config disables the Python version check. - - Reads ``/config/general.yaml`` and looks for ``version.python_version_check``. - Any failure (missing file, unreadable YAML, missing key, missing yaml module) is - treated as "not bypassed" so the default check still fires. - """ - try: - import yaml - - config_path = Path.cwd() / "config" / "general.yaml" - with config_path.open("r") as f: - data = yaml.safe_load(f) or {} - return data.get("version", {}).get("python_version_check") is False - except Exception: - return False - - -_RECOMMENDED_PYTHON_VERSIONS = {(3, 12), (3, 13)} - - -def _emit_python_version_warning(): - current = sys.version_info[:2] - if current in _RECOMMENDED_PYTHON_VERSIONS: - return - if _python_version_check_bypassed(): - return - - py = f"{current[0]}.{current[1]}" - lines = [ - f"PyAuto: Python {py} detected -- first-class support is 3.12 and 3.13.", - "", - f"Things will probably work fine on {py}, but it is not the recommended", - "version and you may hit edge cases.", - ] - if current < (3, 11): - lines.extend( - [ - "", - "Note: JAX acceleration is not available on Python <3.11. Models", - "that pass use_jax=True will error.", - ] - ) - lines.extend( - [ - "", - "Recommended: install Python 3.12 or 3.13.", - "To silence this warning, add to /config/general.yaml:", - "", - " version:", - " python_version_check: False", - ] - ) - - inner_width = max(len(line) for line in lines) - border = "+" + "-" * (inner_width + 4) + "+" - framed = [border] - for line in lines: - framed.append("| " + line.ljust(inner_width) + " |") - framed.append(border) - - print("\n".join(framed), file=sys.stderr) - warnings.warn( - f"PyAuto: running on Python {py}; first-class support is 3.12/3.13. " - f"Suppress this warning via 'version.python_version_check: False' in " - f"config/general.yaml.", - UserWarning, - stacklevel=2, - ) - - -_emit_python_version_warning() - -from . import jax_wrapper -from . import exc -from .tools.decorators import cached_property -from .conf import Config -from .conf import instance -from .json_prior.config import default_prior -from .json_prior.config import make_config_for_class -from .json_prior.config import path_for_class -from .json_prior.config import JSONPriorConfig - -from .setup_colab import for_autolens -from .setup_notebook import setup_notebook -from .test_mode import test_mode_level, is_test_mode, skip_fit_output, skip_visualization, skip_checks -from .workspace import check_version, WorkspaceVersionMismatchError - - -__version__ = "2026.7.9.1" +""" +autonerves — configuration, serialization, and I/O helpers for the PyAuto ecosystem. + +Text-format I/O surfaces: + +- :mod:`autonerves.dictable` — JSON (``output_to_json`` / ``from_json``) +- :mod:`autonerves.fitsable` — FITS (``output_to_fits`` / ``ndarray_via_fits_from``) +- :mod:`autonerves.csvable` — CSV (``output_to_csv`` / ``list_from_csv``) +""" +import sys +import warnings +from pathlib import Path + + +def _python_version_check_bypassed(): + """ + Return True iff the user's workspace config disables the Python version check. + + Reads ``/config/general.yaml`` and looks for ``version.python_version_check``. + Any failure (missing file, unreadable YAML, missing key, missing yaml module) is + treated as "not bypassed" so the default check still fires. + """ + try: + import yaml + + config_path = Path.cwd() / "config" / "general.yaml" + with config_path.open("r") as f: + data = yaml.safe_load(f) or {} + return data.get("version", {}).get("python_version_check") is False + except Exception: + return False + + +_RECOMMENDED_PYTHON_VERSIONS = {(3, 12), (3, 13)} + + +def _emit_python_version_warning(): + current = sys.version_info[:2] + if current in _RECOMMENDED_PYTHON_VERSIONS: + return + if _python_version_check_bypassed(): + return + + py = f"{current[0]}.{current[1]}" + lines = [ + f"PyAuto: Python {py} detected -- first-class support is 3.12 and 3.13.", + "", + f"Things will probably work fine on {py}, but it is not the recommended", + "version and you may hit edge cases.", + ] + if current < (3, 11): + lines.extend( + [ + "", + "Note: JAX acceleration is not available on Python <3.11. Models", + "that pass use_jax=True will error.", + ] + ) + lines.extend( + [ + "", + "Recommended: install Python 3.12 or 3.13.", + "To silence this warning, add to /config/general.yaml:", + "", + " version:", + " python_version_check: False", + ] + ) + + inner_width = max(len(line) for line in lines) + border = "+" + "-" * (inner_width + 4) + "+" + framed = [border] + for line in lines: + framed.append("| " + line.ljust(inner_width) + " |") + framed.append(border) + + print("\n".join(framed), file=sys.stderr) + warnings.warn( + f"PyAuto: running on Python {py}; first-class support is 3.12/3.13. " + f"Suppress this warning via 'version.python_version_check: False' in " + f"config/general.yaml.", + UserWarning, + stacklevel=2, + ) + + +_emit_python_version_warning() + +from . import jax_wrapper +from . import exc +from .tools.decorators import cached_property +from .conf import Config +from .conf import instance +from .json_prior.config import default_prior +from .json_prior.config import make_config_for_class +from .json_prior.config import path_for_class +from .json_prior.config import JSONPriorConfig + +from .setup_colab import for_autolens +from .setup_notebook import setup_notebook +from .test_mode import test_mode_level, is_test_mode, skip_fit_output, skip_visualization, skip_checks +from .workspace import check_version, WorkspaceVersionMismatchError + + +__version__ = "2026.7.9.1" diff --git a/autonerves/class_path.py b/autonerves/class_path.py index 5d3f185..3c257c0 100644 --- a/autonerves/class_path.py +++ b/autonerves/class_path.py @@ -1,61 +1,61 @@ -import builtins -import importlib -import re -from typing import List, Type - - -def get_class_path(cls: type) -> str: - """ - The full import path of the type - """ - if hasattr(cls, "__class_path__"): - cls = cls.__class_path__ - return re.search("'(.*)'", str(cls))[1] - - -def get_class(class_path: str) -> Type[object]: - return GetClass(class_path).cls - - -class GetClass: - def __init__(self, class_path): - self.class_path = class_path - - @property - def _class_path_array(self) -> List[str]: - """ - A list of strings describing the module and class of the - real object represented here - """ - return self.class_path.split(".") - - @property - def _class_name(self) -> str: - """ - The name of the real class - """ - return self._class_path_array[-1] - - @property - def _module_path(self) -> str: - """ - The path of the module containing the real class - """ - return ".".join(self._class_path_array[:-1]) - - @property - def _module(self): - """ - The module containing the real class - """ - try: - return importlib.import_module(self._module_path) - except ValueError: - return builtins - - @property - def cls(self) -> Type[object]: - """ - The class of the real object - """ - return getattr(self._module, self._class_name) +import builtins +import importlib +import re +from typing import List, Type + + +def get_class_path(cls: type) -> str: + """ + The full import path of the type + """ + if hasattr(cls, "__class_path__"): + cls = cls.__class_path__ + return re.search("'(.*)'", str(cls))[1] + + +def get_class(class_path: str) -> Type[object]: + return GetClass(class_path).cls + + +class GetClass: + def __init__(self, class_path): + self.class_path = class_path + + @property + def _class_path_array(self) -> List[str]: + """ + A list of strings describing the module and class of the + real object represented here + """ + return self.class_path.split(".") + + @property + def _class_name(self) -> str: + """ + The name of the real class + """ + return self._class_path_array[-1] + + @property + def _module_path(self) -> str: + """ + The path of the module containing the real class + """ + return ".".join(self._class_path_array[:-1]) + + @property + def _module(self): + """ + The module containing the real class + """ + try: + return importlib.import_module(self._module_path) + except ValueError: + return builtins + + @property + def cls(self) -> Type[object]: + """ + The class of the real object + """ + return getattr(self._module, self._class_name) diff --git a/autonerves/conf.py b/autonerves/conf.py index 0f6a89e..cb30d1b 100644 --- a/autonerves/conf.py +++ b/autonerves/conf.py @@ -1,386 +1,386 @@ -import logging -import logging.config -import os -import shutil -from functools import wraps -from pathlib import Path -from typing import Optional, Union, Dict, MutableMapping - -import yaml - -from autonerves.directory_config import ( - RecursiveConfig, - PriorConfigWrapper, - AbstractConfig, - family, -) -from autonerves.exc import ConfigException -from autonerves.json_prior.config import JSONPriorConfig - -logger = logging.getLogger(__name__) - -LOGGING_CONFIG_FILE = "logging.yaml" - - -def get_matplotlib_backend(): - try: - return instance["visualize"]["general"]["general"]["backend"] - except KeyError: - return "default" - - -class DictWrapper(MutableMapping): - def __delitem__(self, v) -> None: - del self._dict[v] - - def __len__(self) -> int: - return len(self._dict) - - def __iter__(self): - return iter(self._dict) - - def __init__(self, paths): - self._dict = dict() - self.paths = paths - - def __contains__(self, item): - return item.lower() in self._dict - - def items(self): - return self._dict.items() - - def __setitem__(self, key, value): - if isinstance(key, str): - key = key.lower() - self._dict[key] = value - - def __getitem__(self, key): - if isinstance(key, str): - key = key.lower() - try: - return self._dict[key] - except KeyError: - raise KeyError(f"key {key} not found in paths {self.paths_string}") - - @property - def paths_string(self): - return "\n".join(map(str, self.paths)) - - def __repr__(self): - return repr(self._dict) - - def family(self, cls): - for item in family(cls): - try: - return self[item] - except KeyError: - pass - raise KeyError( - f"config for {cls} or its parents not found in paths {self.paths_string}" - ) - - -class Config: - def __init__(self, *config_paths, output_path: Union[str, Path] = "output"): - """ - Singleton to manage configuration. - - Configuration is loaded using the __getitem__ syntax where the key entered - can refer to a directory, file, section or item. - - Configuration is first attempted to be loaded from the directory indicated by the first - config_path. If no configuration is found the second directory is searched and so on. - This allows a default configuration to be defined with additional configuration overriding - it. - - Parameters - ---------- - config_paths - Indicate directories where configuration is defined, in the order of priority with - configuration in the first config_path overriding configuration in later config - paths - output_path - The path where data should be saved. - """ - for config_path in config_paths: - if Path(config_path).name == "output": - logger.warning( - f"{config_path} passed as config path. Did you mean to use output_path={config_path}?" - ) - - self._prior_config = None - - self._configs = list() - self._dict = DictWrapper(self.paths) - - self.configs = list(map(RecursiveConfig, config_paths)) - - self.output_path = output_path - - @property - def dict(self): - """ - A dictionary containing configuration loaded from directories and files. - - Lazily recurse directories to load configuration. - """ - if self._dict is None: - self._dict = DictWrapper(self.paths) - - def recurse_config(config, d): - try: - for key, value in config.items(): - if isinstance(value, AbstractConfig): - if key not in d: - d[key] = DictWrapper(self.paths) - recurse_config(value, d=d[key]) - else: - d[key] = value - except KeyError as e: - logger.debug(e) - - for config_ in reversed(self._configs): - recurse_config(config_, self._dict) - return self._dict - - def configure_logging(self): - """ - Set the most up to date logging configuration - """ - logging_config = self.logging_config - try: - if logging_config is not None: - logging.config.dictConfig(logging_config) - except ValueError as e: - logger.warning(e) - - @property - def logging_config(self) -> Optional[Dict]: - """ - Loading logging configuration from a YAML file - from the most recently added config directory - for which it exists. - """ - for config in self.configs: - path = config.path - try: - if LOGGING_CONFIG_FILE in os.listdir(config.path): - with open(path / LOGGING_CONFIG_FILE) as f: - return yaml.safe_load(f) - except FileNotFoundError: - logger.debug(f"No configuration found at path {config.path}") - return None - - @property - def configs(self): - return self._configs - - @configs.setter - def configs(self, configs): - """ - When the list of configs is updated this invalidates the current config - dictionary - """ - self._prior_config = None - self._dict = None - self._configs = configs - - def __getitem__(self, item): - return self.dict[item] - - def __iter__(self): - return iter(self.dict) - - @property - def paths(self): - return [config.path for config in self._configs] - - @property - def prior_config(self) -> PriorConfigWrapper: - """ - Configuration for priors. This indicates, for example, the mean and the width of priors - for the attributes of given classes. - """ - if self._prior_config is None: - self._prior_config = PriorConfigWrapper( - [JSONPriorConfig.from_directory(path / "priors") for path in self.paths] - ) - return self._prior_config - - def push( - self, - new_path: Union[str, Path], - output_path: Optional[str] = None, - keep_first: bool = False, - ): - """ - Push a new configuration path. This overrides the existing config - paths, with existing configs being used as a backup when a value - cannot be found in an overriding config. - - Parameters - ---------- - new_path - A path to config directory - output_path - The path at which data should be output. If this is None then it remains - unchanged - keep_first - If True the current priority configuration mains such. - - Raises - ------ - ConfigException - If the pushed path does not exist or does not contain at least one file - with an expected configuration suffix - """ - logger.debug(f"Pushing new config with path {new_path}") - - if not Path(new_path).exists(): - raise ConfigException(f"{new_path} does not exist") - - CONFIG_SUFFIXES = (".yml", ".ini", ".json", ".yaml") - - has_config = False - for dirpath, _, files in os.walk(new_path): - for file in files: - if file.endswith(CONFIG_SUFFIXES): - has_config = True - break - if has_config: - break - - if not has_config: - raise ConfigException( - f"{new_path} does not contain any files ending with {'/'.join(CONFIG_SUFFIXES)} recursively" - ) - - self.output_path = output_path or self.output_path - - try: - if self.configs[0] == new_path or ( - keep_first and len(self.configs) > 1 and self.configs[1] == new_path - ): - return - except IndexError: - pass - - new_config = RecursiveConfig(new_path) - - configs = list(filter(lambda config: config != new_config, self.configs)) - if keep_first: - self.configs = configs[:1] + [new_config] + configs[1:] - else: - self.configs = [new_config] + configs - - self.configure_logging() - - def register(self, file: str): - """ - Add defaults for a given project - - Parameters - ---------- - file - The path to the project's __init__ - """ - self.push(Path(file).parent / "config", keep_first=True) - - -current_directory = Path(os.getcwd()) - -default = Config( - current_directory / "config", output_path=current_directory / "output/" -) - -instance = default - - -def output_path_for_test(temporary_path="temp", remove=True): - """ - Temporarily change the output path for the scope of a function - (e.g. a test). Remove the files after the test has completed - execution. - - Parameters - ---------- - temporary_path - The path to temporarily output files to - remove - Should the path be removed? - - Returns - ------- - The original function, decorated - """ - - def remove_(): - if remove: - shutil.rmtree(temporary_path, ignore_errors=True) - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - remove_() - original_path = instance.output_path - instance.output_path = temporary_path - - result = func(*args, **kwargs) - - remove_() - instance.output_path = original_path - - return result - - return wrapper - - return decorator - - -class NoValue: - pass - - -def with_config(*path: str, value): - """ - Create a decorator that swaps a value in configuration - defined by path for the scope of a test. - - Parameters - ---------- - path - A path through config. e.g. "general", "output", "identifier_version" - value - The value to temporarily set for the config field - - Returns - ------- - A decorator - """ - - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - config = instance - for string in path[:-1]: - config = config[string] - - try: - original_value = config[path[-1]] - except KeyError: - original_value = NoValue - - config[path[-1]] = value - - result = func(*args, **kwargs) - - if original_value is NoValue: - del config[path[-1]] - else: - config[path[-1]] = original_value - - return result - - return wrapper - - return decorator +import logging +import logging.config +import os +import shutil +from functools import wraps +from pathlib import Path +from typing import Optional, Union, Dict, MutableMapping + +import yaml + +from autonerves.directory_config import ( + RecursiveConfig, + PriorConfigWrapper, + AbstractConfig, + family, +) +from autonerves.exc import ConfigException +from autonerves.json_prior.config import JSONPriorConfig + +logger = logging.getLogger(__name__) + +LOGGING_CONFIG_FILE = "logging.yaml" + + +def get_matplotlib_backend(): + try: + return instance["visualize"]["general"]["general"]["backend"] + except KeyError: + return "default" + + +class DictWrapper(MutableMapping): + def __delitem__(self, v) -> None: + del self._dict[v] + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self): + return iter(self._dict) + + def __init__(self, paths): + self._dict = dict() + self.paths = paths + + def __contains__(self, item): + return item.lower() in self._dict + + def items(self): + return self._dict.items() + + def __setitem__(self, key, value): + if isinstance(key, str): + key = key.lower() + self._dict[key] = value + + def __getitem__(self, key): + if isinstance(key, str): + key = key.lower() + try: + return self._dict[key] + except KeyError: + raise KeyError(f"key {key} not found in paths {self.paths_string}") + + @property + def paths_string(self): + return "\n".join(map(str, self.paths)) + + def __repr__(self): + return repr(self._dict) + + def family(self, cls): + for item in family(cls): + try: + return self[item] + except KeyError: + pass + raise KeyError( + f"config for {cls} or its parents not found in paths {self.paths_string}" + ) + + +class Config: + def __init__(self, *config_paths, output_path: Union[str, Path] = "output"): + """ + Singleton to manage configuration. + + Configuration is loaded using the __getitem__ syntax where the key entered + can refer to a directory, file, section or item. + + Configuration is first attempted to be loaded from the directory indicated by the first + config_path. If no configuration is found the second directory is searched and so on. + This allows a default configuration to be defined with additional configuration overriding + it. + + Parameters + ---------- + config_paths + Indicate directories where configuration is defined, in the order of priority with + configuration in the first config_path overriding configuration in later config + paths + output_path + The path where data should be saved. + """ + for config_path in config_paths: + if Path(config_path).name == "output": + logger.warning( + f"{config_path} passed as config path. Did you mean to use output_path={config_path}?" + ) + + self._prior_config = None + + self._configs = list() + self._dict = DictWrapper(self.paths) + + self.configs = list(map(RecursiveConfig, config_paths)) + + self.output_path = output_path + + @property + def dict(self): + """ + A dictionary containing configuration loaded from directories and files. + + Lazily recurse directories to load configuration. + """ + if self._dict is None: + self._dict = DictWrapper(self.paths) + + def recurse_config(config, d): + try: + for key, value in config.items(): + if isinstance(value, AbstractConfig): + if key not in d: + d[key] = DictWrapper(self.paths) + recurse_config(value, d=d[key]) + else: + d[key] = value + except KeyError as e: + logger.debug(e) + + for config_ in reversed(self._configs): + recurse_config(config_, self._dict) + return self._dict + + def configure_logging(self): + """ + Set the most up to date logging configuration + """ + logging_config = self.logging_config + try: + if logging_config is not None: + logging.config.dictConfig(logging_config) + except ValueError as e: + logger.warning(e) + + @property + def logging_config(self) -> Optional[Dict]: + """ + Loading logging configuration from a YAML file + from the most recently added config directory + for which it exists. + """ + for config in self.configs: + path = config.path + try: + if LOGGING_CONFIG_FILE in os.listdir(config.path): + with open(path / LOGGING_CONFIG_FILE) as f: + return yaml.safe_load(f) + except FileNotFoundError: + logger.debug(f"No configuration found at path {config.path}") + return None + + @property + def configs(self): + return self._configs + + @configs.setter + def configs(self, configs): + """ + When the list of configs is updated this invalidates the current config + dictionary + """ + self._prior_config = None + self._dict = None + self._configs = configs + + def __getitem__(self, item): + return self.dict[item] + + def __iter__(self): + return iter(self.dict) + + @property + def paths(self): + return [config.path for config in self._configs] + + @property + def prior_config(self) -> PriorConfigWrapper: + """ + Configuration for priors. This indicates, for example, the mean and the width of priors + for the attributes of given classes. + """ + if self._prior_config is None: + self._prior_config = PriorConfigWrapper( + [JSONPriorConfig.from_directory(path / "priors") for path in self.paths] + ) + return self._prior_config + + def push( + self, + new_path: Union[str, Path], + output_path: Optional[str] = None, + keep_first: bool = False, + ): + """ + Push a new configuration path. This overrides the existing config + paths, with existing configs being used as a backup when a value + cannot be found in an overriding config. + + Parameters + ---------- + new_path + A path to config directory + output_path + The path at which data should be output. If this is None then it remains + unchanged + keep_first + If True the current priority configuration mains such. + + Raises + ------ + ConfigException + If the pushed path does not exist or does not contain at least one file + with an expected configuration suffix + """ + logger.debug(f"Pushing new config with path {new_path}") + + if not Path(new_path).exists(): + raise ConfigException(f"{new_path} does not exist") + + CONFIG_SUFFIXES = (".yml", ".ini", ".json", ".yaml") + + has_config = False + for dirpath, _, files in os.walk(new_path): + for file in files: + if file.endswith(CONFIG_SUFFIXES): + has_config = True + break + if has_config: + break + + if not has_config: + raise ConfigException( + f"{new_path} does not contain any files ending with {'/'.join(CONFIG_SUFFIXES)} recursively" + ) + + self.output_path = output_path or self.output_path + + try: + if self.configs[0] == new_path or ( + keep_first and len(self.configs) > 1 and self.configs[1] == new_path + ): + return + except IndexError: + pass + + new_config = RecursiveConfig(new_path) + + configs = list(filter(lambda config: config != new_config, self.configs)) + if keep_first: + self.configs = configs[:1] + [new_config] + configs[1:] + else: + self.configs = [new_config] + configs + + self.configure_logging() + + def register(self, file: str): + """ + Add defaults for a given project + + Parameters + ---------- + file + The path to the project's __init__ + """ + self.push(Path(file).parent / "config", keep_first=True) + + +current_directory = Path(os.getcwd()) + +default = Config( + current_directory / "config", output_path=current_directory / "output/" +) + +instance = default + + +def output_path_for_test(temporary_path="temp", remove=True): + """ + Temporarily change the output path for the scope of a function + (e.g. a test). Remove the files after the test has completed + execution. + + Parameters + ---------- + temporary_path + The path to temporarily output files to + remove + Should the path be removed? + + Returns + ------- + The original function, decorated + """ + + def remove_(): + if remove: + shutil.rmtree(temporary_path, ignore_errors=True) + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + remove_() + original_path = instance.output_path + instance.output_path = temporary_path + + result = func(*args, **kwargs) + + remove_() + instance.output_path = original_path + + return result + + return wrapper + + return decorator + + +class NoValue: + pass + + +def with_config(*path: str, value): + """ + Create a decorator that swaps a value in configuration + defined by path for the scope of a test. + + Parameters + ---------- + path + A path through config. e.g. "general", "output", "identifier_version" + value + The value to temporarily set for the config field + + Returns + ------- + A decorator + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + config = instance + for string in path[:-1]: + config = config[string] + + try: + original_value = config[path[-1]] + except KeyError: + original_value = NoValue + + config[path[-1]] = value + + result = func(*args, **kwargs) + + if original_value is NoValue: + del config[path[-1]] + else: + config[path[-1]] = original_value + + return result + + return wrapper + + return decorator diff --git a/autonerves/directory_config.py b/autonerves/directory_config.py index d7be68b..232af9c 100644 --- a/autonerves/directory_config.py +++ b/autonerves/directory_config.py @@ -1,243 +1,243 @@ -import configparser -import os -from abc import abstractmethod, ABC -from pathlib import Path - -import yaml - -from autonerves import exc - - -class AbstractConfig(ABC): - @abstractmethod - def _getitem(self, item): - pass - - def __getitem__(self, item): - if isinstance(item, int): - return self.items()[item] - return self._getitem(item) - - def items(self): - return [(key, self[key]) for key in self.keys()] - - def __len__(self): - return len(self.items()) - - @abstractmethod - def keys(self): - pass - - def family(self, cls): - for cls in family(cls): - key = cls.__name__ - try: - return self[key] - except (KeyError, configparser.NoOptionError): - pass - raise KeyError(f"No configuration found for {cls.__name__}") - - def dict(self): - d = {} - for key in self.keys(): - value = self[key] - if isinstance(value, AbstractConfig): - value = value.dict() - d[key] = value - return d - - -class DictConfig(AbstractConfig): - def keys(self): - return self.d.keys() - - def __init__(self, d): - self.d = d - - def __getitem__(self, item): - value = self.d[item] - if isinstance(value, dict): - return DictConfig(value) - return value - - def _getitem(self, item): - return self[item] - - def items(self): - for key in self.d: - yield key, self[key] - - -class YAMLConfig(AbstractConfig): - def __init__(self, path): - with open(path) as f: - self._dict = yaml.safe_load(f) - - def _getitem(self, item): - value = self._dict[item] - if isinstance(value, dict): - return DictConfig(value) - return value - - def keys(self): - if not isinstance(self._dict, dict): - return iter(()) - return self._dict.keys() - - -class SectionConfig(AbstractConfig): - def __init__(self, path, parser, section): - self.path = path - self.section = section - self.parser = parser - - def keys(self): - with open(self.path) as f: - string = f.read() - - lines = string.split("\n") - is_section = False - for line in lines: - if line == f"[{self.section}]": - is_section = True - continue - if line.startswith("["): - is_section = False - continue - if is_section and "=" in line: - yield line.split("=")[0] - - def _getitem(self, item): - try: - result = self.parser.get(self.section, item) - if result.lower() == "true": - return True - if result.lower() == "false": - return False - if result.lower() in ("none", "null"): - return None - if result.isdigit(): - return int(result) - try: - return float(result) - except ValueError: - return result - except (configparser.NoSectionError, configparser.NoOptionError): - raise KeyError(f"No configuration found for {item} at path {self.path}") - - -class NamedConfig(AbstractConfig): - def __init__(self, config_path): - """ - Parses generic config - - Parameters - ---------- - config_path - The path to the config file - """ - self.path = config_path - self.parser = configparser.ConfigParser() - self.parser.read(self.path) - - def keys(self): - return self.parser.sections() - - def _getitem(self, item): - return SectionConfig( - self.path, - self.parser, - item, - ) - - -class RecursiveConfig(AbstractConfig): - def __init__(self, path): - self.path = Path(path) - self._listing = None - - @property - def listing(self): - if self._listing is None: - try: - self._listing = set(os.listdir(self.path)) - except FileNotFoundError: - self._listing = set() - return self._listing - - def keys(self): - try: - return [ - path.split(".")[0] - for path in self.listing - if all( - [ - path != "priors", - len(path.split(".")[0]) != 0, - (self.path / path).is_dir() - or path.endswith(".ini") - or path.endswith(".yaml") - or path.endswith(".yml"), - ] - ) - ] - except FileNotFoundError as e: - raise KeyError(f"No configuration found at {self.path}") from e - - def __eq__(self, other): - return str(self) == str(other) - - def __str__(self): - return str(self.path) - - def __repr__(self): - return f"<{self.__class__.__name__} {self.path}>" - - def _getitem(self, item): - listing = self.listing - ini_name = f"{item}.ini" - if ini_name in listing: - return NamedConfig(self.path / ini_name) - yml_name = f"{item}.yml" - if yml_name in listing: - return YAMLConfig(self.path / yml_name) - yaml_name = f"{item}.yaml" - if yaml_name in listing: - return YAMLConfig(self.path / yaml_name) - if item in listing and (self.path / item).is_dir(): - return RecursiveConfig(self.path / item) - raise KeyError(f"No configuration found for {item} at path {self.path}") - - -class PriorConfigWrapper: - def __init__(self, prior_configs): - self.prior_configs = prior_configs - - def for_class_and_suffix_path(self, cls, path): - for config in self.prior_configs: - try: - return config.for_class_and_suffix_path(cls, path) - except KeyError: - pass - directories = " ".join(str(config.directory) for config in self.prior_configs) - - print() - - raise exc.ConfigException( - f"No prior config found for class: \n\n" - f"{cls.__name__} \n\n" - f"For parameter name and path: \n\n " - f"{'.'.join(path)} \n\n " - f"In any of the following directories:\n\n" - f"{directories}\n\n" - f"Either add configuration for the parameter or a type annotation for a class with valid configuration.\n\n" - f"The following readthedocs page explains prior configuration files in PyAutoFit and will help you fix " - f"the error https://pyautofit.readthedocs.io/en/latest/general/adding_a_model_component.html" - ) - - -def family(current_class): - yield current_class - for next_class in current_class.__bases__: - for val in family(next_class): - yield val +import configparser +import os +from abc import abstractmethod, ABC +from pathlib import Path + +import yaml + +from autonerves import exc + + +class AbstractConfig(ABC): + @abstractmethod + def _getitem(self, item): + pass + + def __getitem__(self, item): + if isinstance(item, int): + return self.items()[item] + return self._getitem(item) + + def items(self): + return [(key, self[key]) for key in self.keys()] + + def __len__(self): + return len(self.items()) + + @abstractmethod + def keys(self): + pass + + def family(self, cls): + for cls in family(cls): + key = cls.__name__ + try: + return self[key] + except (KeyError, configparser.NoOptionError): + pass + raise KeyError(f"No configuration found for {cls.__name__}") + + def dict(self): + d = {} + for key in self.keys(): + value = self[key] + if isinstance(value, AbstractConfig): + value = value.dict() + d[key] = value + return d + + +class DictConfig(AbstractConfig): + def keys(self): + return self.d.keys() + + def __init__(self, d): + self.d = d + + def __getitem__(self, item): + value = self.d[item] + if isinstance(value, dict): + return DictConfig(value) + return value + + def _getitem(self, item): + return self[item] + + def items(self): + for key in self.d: + yield key, self[key] + + +class YAMLConfig(AbstractConfig): + def __init__(self, path): + with open(path) as f: + self._dict = yaml.safe_load(f) + + def _getitem(self, item): + value = self._dict[item] + if isinstance(value, dict): + return DictConfig(value) + return value + + def keys(self): + if not isinstance(self._dict, dict): + return iter(()) + return self._dict.keys() + + +class SectionConfig(AbstractConfig): + def __init__(self, path, parser, section): + self.path = path + self.section = section + self.parser = parser + + def keys(self): + with open(self.path) as f: + string = f.read() + + lines = string.split("\n") + is_section = False + for line in lines: + if line == f"[{self.section}]": + is_section = True + continue + if line.startswith("["): + is_section = False + continue + if is_section and "=" in line: + yield line.split("=")[0] + + def _getitem(self, item): + try: + result = self.parser.get(self.section, item) + if result.lower() == "true": + return True + if result.lower() == "false": + return False + if result.lower() in ("none", "null"): + return None + if result.isdigit(): + return int(result) + try: + return float(result) + except ValueError: + return result + except (configparser.NoSectionError, configparser.NoOptionError): + raise KeyError(f"No configuration found for {item} at path {self.path}") + + +class NamedConfig(AbstractConfig): + def __init__(self, config_path): + """ + Parses generic config + + Parameters + ---------- + config_path + The path to the config file + """ + self.path = config_path + self.parser = configparser.ConfigParser() + self.parser.read(self.path) + + def keys(self): + return self.parser.sections() + + def _getitem(self, item): + return SectionConfig( + self.path, + self.parser, + item, + ) + + +class RecursiveConfig(AbstractConfig): + def __init__(self, path): + self.path = Path(path) + self._listing = None + + @property + def listing(self): + if self._listing is None: + try: + self._listing = set(os.listdir(self.path)) + except FileNotFoundError: + self._listing = set() + return self._listing + + def keys(self): + try: + return [ + path.split(".")[0] + for path in self.listing + if all( + [ + path != "priors", + len(path.split(".")[0]) != 0, + (self.path / path).is_dir() + or path.endswith(".ini") + or path.endswith(".yaml") + or path.endswith(".yml"), + ] + ) + ] + except FileNotFoundError as e: + raise KeyError(f"No configuration found at {self.path}") from e + + def __eq__(self, other): + return str(self) == str(other) + + def __str__(self): + return str(self.path) + + def __repr__(self): + return f"<{self.__class__.__name__} {self.path}>" + + def _getitem(self, item): + listing = self.listing + ini_name = f"{item}.ini" + if ini_name in listing: + return NamedConfig(self.path / ini_name) + yml_name = f"{item}.yml" + if yml_name in listing: + return YAMLConfig(self.path / yml_name) + yaml_name = f"{item}.yaml" + if yaml_name in listing: + return YAMLConfig(self.path / yaml_name) + if item in listing and (self.path / item).is_dir(): + return RecursiveConfig(self.path / item) + raise KeyError(f"No configuration found for {item} at path {self.path}") + + +class PriorConfigWrapper: + def __init__(self, prior_configs): + self.prior_configs = prior_configs + + def for_class_and_suffix_path(self, cls, path): + for config in self.prior_configs: + try: + return config.for_class_and_suffix_path(cls, path) + except KeyError: + pass + directories = " ".join(str(config.directory) for config in self.prior_configs) + + print() + + raise exc.ConfigException( + f"No prior config found for class: \n\n" + f"{cls.__name__} \n\n" + f"For parameter name and path: \n\n " + f"{'.'.join(path)} \n\n " + f"In any of the following directories:\n\n" + f"{directories}\n\n" + f"Either add configuration for the parameter or a type annotation for a class with valid configuration.\n\n" + f"The following readthedocs page explains prior configuration files in PyAutoFit and will help you fix " + f"the error https://pyautofit.readthedocs.io/en/latest/general/adding_a_model_component.html" + ) + + +def family(current_class): + yield current_class + for next_class in current_class.__bases__: + for val in family(next_class): + yield val diff --git a/autonerves/exc.py b/autonerves/exc.py index dc72f91..46149a3 100644 --- a/autonerves/exc.py +++ b/autonerves/exc.py @@ -1,6 +1,6 @@ -class ConfigException(Exception): - pass - - -class PriorException(Exception): - pass +class ConfigException(Exception): + pass + + +class PriorException(Exception): + pass diff --git a/autonerves/fitsable.py b/autonerves/fitsable.py index 7817769..1f44679 100644 --- a/autonerves/fitsable.py +++ b/autonerves/fitsable.py @@ -1,242 +1,242 @@ -from __future__ import annotations - -from enum import Enum -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - try: - from astropy.io import fits - except ImportError: - pass - -try: - from astropy.io import fits -except ImportError: - pass - -import numpy as np -from pathlib import Path -from typing import Dict, Optional, Union, List - - -def hdu_list_for_output_from( - values_list: List[np.ndarray], - header_dict: Optional[dict] = None, - ext_name_list: Optional[List[str]] = None, -) -> fits.HDUList: - """ - Returns the HDU list which can be used to output arrays to a .fits file. - - The output .fits files may contain multiple HDUs comprising different images. Conventionally, the first array, - the `PrimaryHDU`, contains the 2D mask applied to the data and the remaining HDUs contain the data itself. - The mask is used to add information to the header, for example the pixel scale of the data. - - Each HDU contains its `ext_name` in the header, which is visible when the .fits file is loaded in DS9. - - Parameters - ---------- - values - The 1D or 2D array that is written to fits. - header_dict - A dictionary of values that are written to the header of the .fits file. - ext_name_list - The names of the extension in the fits file, which displays in the header of the fits file and is visible - for example when the .fits is loaded in DS9. - - Returns - ------- - The HDU list containing the data and its header which can then be written to .fits. - - Examples - -------- - data = np.ones((5,5)) - noise_map = np.ones((5,5)) - - hdu_list_for_output_from( - values_list=[data, noise_map] - header_dict={"Example": 0.5}, - ext_name_list=["data", "noise_map"] - ) - """ - hdu_list = [] - - header = fits.Header() - - if header_dict is not None: - for key, value in header_dict.items(): - # Convert enum to its string value if needed - key_str = key.value if isinstance(key, Enum) else key - try: - header.append((key_str, value, [""])) - except ValueError: - header.append((key_str, float(value), [""])) - - for i, values in enumerate(values_list): - - if ext_name_list is not None: - header["EXTNAME"] = ext_name_list[i].upper() - - # Convert from JAX - try: - values = np.array(values.array) - except AttributeError: - values = np.array(values) - - if i == 0: - hdu_list.append(fits.PrimaryHDU(values, header=header)) - else: - hdu_list.append(fits.ImageHDU(values, header=header)) - - return fits.HDUList(hdus=hdu_list) - -def output_to_fits( - values: np.ndarray, - file_path: Union[Path, str], - overwrite: bool = False, - header_dict: Optional[dict] = None, - ext_name: Optional[str] = None, -): - """ - Write a NumPy array to a .fits file. - - Parameters - ---------- - values - The numpy array of values that is written to fits. - file_path - The full path of the file that is output, including the file name and ``.fits`` extension. - overwrite - If `True` and a file already exists with the input file_path the .fits file is overwritten. If `False`, an - error is raised. - header_dict - A dictionary of values that are written to the header of the .fits file. - ext_name - The name of the extension in the fits file, which displays in the header of the fits file and is visible. - - Examples - -------- - values = np.ones((5,5)) - numpy_array_to_fits(values=values, file_path='/path/to/file/filename.fits', overwrite=True) - """ - - hdu = hdu_list_for_output_from( - values_list=[values], - header_dict=header_dict, - ext_name_list=[ext_name] if ext_name is not None else None, - ) - - write_hdu_list(hdu, file_path=file_path, overwrite=overwrite) - -def write_hdu_list(hdu_list, file_path, overwrite=False): - """Write an ``HDUList`` to a FITS file, creating directories as needed. - - Parameters - ---------- - hdu_list - The ``astropy.io.fits.HDUList`` to write. - file_path : str or Path - Full output path including ``.fits`` extension. - overwrite : bool - If ``True`` an existing file is replaced. - """ - file_path = Path(file_path) - file_path.parent.mkdir(parents=True, exist_ok=True) - if overwrite and file_path.is_file(): - file_path.unlink() - hdu_list.writeto(file_path) - - -def ndarray_via_hdu_from(hdu): - """ - Returns an ``Array2D`` by from a `PrimaryHDU` object which has been loaded via `astropy.fits` - - This assumes that the `header` of the `PrimaryHDU` contains an entry named `PIXSCALE` which gives the - pixel-scale of the array. - - For a full description of ``Array2D`` objects, including a description of the ``slim`` and ``native`` attribute - used by the API, see - the :meth:`Array2D class API documentation `. - - Parameters - ---------- - primary_hdu - The `PrimaryHDU` object which has already been loaded from a .fits file via `astropy.fits` and contains - the array data and the pixel-scale in the header with an entry named `PIXSCALE`. - origin - The (y,x) scaled units origin of the coordinate system. - - Examples - -------- - - .. code-block:: python - - from astropy.io import fits - import autoarray as aa - - primary_hdu = fits.open("path/to/file.fits") - - array_2d = aa.Array2D.from_primary_hdu( - primary_hdu=primary_hdu, - ) - """ - values = hdu.data.astype("float") - return values - - -def ndarray_via_fits_from( - file_path: Union[Path, str], hdu: int, do_not_scale_image_data: bool = False -): - """ - Read a 2D NumPy array from a .fits file. - - Parameters - ---------- - file_path - The full path of the file that is loaded, including the file name and ``.fits`` extension. - hdu - The HDU extension of the array that is loaded from the .fits file. - do_not_scale_image_data - If True, the .fits file is not rescaled automatically based on the .fits header info. - - Returns - ------- - ndarray - The NumPy array that is loaded from the .fits file. - - Examples - -------- - array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) - """ - hdu_list = fits.open(file_path, do_not_scale_image_data=do_not_scale_image_data) - return ndarray_via_hdu_from(hdu_list[hdu]) - - -def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: - """ - Read a 2D NumPy array from a .fits file. - - Parameters - ---------- - file_path - The full path of the file that is loaded, including the file name and ``.fits`` extension. - hdu - The HDU extension of the array that is loaded from the .fits file. - do_not_scale_image_data - If True, the .fits file is not rescaled automatically based on the .fits header info. - - Returns - ------- - dict - The header dictionary. - - Examples - -------- - array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) - """ - hdu_list = fits.open(file_path) - return hdu_list[hdu].header - - - - - +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + try: + from astropy.io import fits + except ImportError: + pass + +try: + from astropy.io import fits +except ImportError: + pass + +import numpy as np +from pathlib import Path +from typing import Dict, Optional, Union, List + + +def hdu_list_for_output_from( + values_list: List[np.ndarray], + header_dict: Optional[dict] = None, + ext_name_list: Optional[List[str]] = None, +) -> fits.HDUList: + """ + Returns the HDU list which can be used to output arrays to a .fits file. + + The output .fits files may contain multiple HDUs comprising different images. Conventionally, the first array, + the `PrimaryHDU`, contains the 2D mask applied to the data and the remaining HDUs contain the data itself. + The mask is used to add information to the header, for example the pixel scale of the data. + + Each HDU contains its `ext_name` in the header, which is visible when the .fits file is loaded in DS9. + + Parameters + ---------- + values + The 1D or 2D array that is written to fits. + header_dict + A dictionary of values that are written to the header of the .fits file. + ext_name_list + The names of the extension in the fits file, which displays in the header of the fits file and is visible + for example when the .fits is loaded in DS9. + + Returns + ------- + The HDU list containing the data and its header which can then be written to .fits. + + Examples + -------- + data = np.ones((5,5)) + noise_map = np.ones((5,5)) + + hdu_list_for_output_from( + values_list=[data, noise_map] + header_dict={"Example": 0.5}, + ext_name_list=["data", "noise_map"] + ) + """ + hdu_list = [] + + header = fits.Header() + + if header_dict is not None: + for key, value in header_dict.items(): + # Convert enum to its string value if needed + key_str = key.value if isinstance(key, Enum) else key + try: + header.append((key_str, value, [""])) + except ValueError: + header.append((key_str, float(value), [""])) + + for i, values in enumerate(values_list): + + if ext_name_list is not None: + header["EXTNAME"] = ext_name_list[i].upper() + + # Convert from JAX + try: + values = np.array(values.array) + except AttributeError: + values = np.array(values) + + if i == 0: + hdu_list.append(fits.PrimaryHDU(values, header=header)) + else: + hdu_list.append(fits.ImageHDU(values, header=header)) + + return fits.HDUList(hdus=hdu_list) + +def output_to_fits( + values: np.ndarray, + file_path: Union[Path, str], + overwrite: bool = False, + header_dict: Optional[dict] = None, + ext_name: Optional[str] = None, +): + """ + Write a NumPy array to a .fits file. + + Parameters + ---------- + values + The numpy array of values that is written to fits. + file_path + The full path of the file that is output, including the file name and ``.fits`` extension. + overwrite + If `True` and a file already exists with the input file_path the .fits file is overwritten. If `False`, an + error is raised. + header_dict + A dictionary of values that are written to the header of the .fits file. + ext_name + The name of the extension in the fits file, which displays in the header of the fits file and is visible. + + Examples + -------- + values = np.ones((5,5)) + numpy_array_to_fits(values=values, file_path='/path/to/file/filename.fits', overwrite=True) + """ + + hdu = hdu_list_for_output_from( + values_list=[values], + header_dict=header_dict, + ext_name_list=[ext_name] if ext_name is not None else None, + ) + + write_hdu_list(hdu, file_path=file_path, overwrite=overwrite) + +def write_hdu_list(hdu_list, file_path, overwrite=False): + """Write an ``HDUList`` to a FITS file, creating directories as needed. + + Parameters + ---------- + hdu_list + The ``astropy.io.fits.HDUList`` to write. + file_path : str or Path + Full output path including ``.fits`` extension. + overwrite : bool + If ``True`` an existing file is replaced. + """ + file_path = Path(file_path) + file_path.parent.mkdir(parents=True, exist_ok=True) + if overwrite and file_path.is_file(): + file_path.unlink() + hdu_list.writeto(file_path) + + +def ndarray_via_hdu_from(hdu): + """ + Returns an ``Array2D`` by from a `PrimaryHDU` object which has been loaded via `astropy.fits` + + This assumes that the `header` of the `PrimaryHDU` contains an entry named `PIXSCALE` which gives the + pixel-scale of the array. + + For a full description of ``Array2D`` objects, including a description of the ``slim`` and ``native`` attribute + used by the API, see + the :meth:`Array2D class API documentation `. + + Parameters + ---------- + primary_hdu + The `PrimaryHDU` object which has already been loaded from a .fits file via `astropy.fits` and contains + the array data and the pixel-scale in the header with an entry named `PIXSCALE`. + origin + The (y,x) scaled units origin of the coordinate system. + + Examples + -------- + + .. code-block:: python + + from astropy.io import fits + import autoarray as aa + + primary_hdu = fits.open("path/to/file.fits") + + array_2d = aa.Array2D.from_primary_hdu( + primary_hdu=primary_hdu, + ) + """ + values = hdu.data.astype("float") + return values + + +def ndarray_via_fits_from( + file_path: Union[Path, str], hdu: int, do_not_scale_image_data: bool = False +): + """ + Read a 2D NumPy array from a .fits file. + + Parameters + ---------- + file_path + The full path of the file that is loaded, including the file name and ``.fits`` extension. + hdu + The HDU extension of the array that is loaded from the .fits file. + do_not_scale_image_data + If True, the .fits file is not rescaled automatically based on the .fits header info. + + Returns + ------- + ndarray + The NumPy array that is loaded from the .fits file. + + Examples + -------- + array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) + """ + hdu_list = fits.open(file_path, do_not_scale_image_data=do_not_scale_image_data) + return ndarray_via_hdu_from(hdu_list[hdu]) + + +def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: + """ + Read a 2D NumPy array from a .fits file. + + Parameters + ---------- + file_path + The full path of the file that is loaded, including the file name and ``.fits`` extension. + hdu + The HDU extension of the array that is loaded from the .fits file. + do_not_scale_image_data + If True, the .fits file is not rescaled automatically based on the .fits header info. + + Returns + ------- + dict + The header dictionary. + + Examples + -------- + array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) + """ + hdu_list = fits.open(file_path) + return hdu_list[hdu].header + + + + + diff --git a/autonerves/json_prior/config.py b/autonerves/json_prior/config.py index e75df53..0cffaab 100644 --- a/autonerves/json_prior/config.py +++ b/autonerves/json_prior/config.py @@ -1,244 +1,244 @@ -import inspect -import json -import logging -from collections.abc import Sized -from pathlib import Path -from typing import List, Type, Tuple - -import yaml - -from autonerves.directory_config import family - -logger = logging.getLogger(__name__) - -default_prior = { - "type": "Uniform", - "lower_limit": 0.0, - "upper_limit": 1.0, - "width_modifier": {"type": "Absolute", "value": 0.2}, - "limits": {"lower": 0.0, "upper": 1.0}, -} - - -def make_config_for_class(cls): - path = path_for_class(cls) - arg_spec = inspect.getfullargspec(cls) - arguments = arg_spec.args[1:] - defaults = list(reversed(arg_spec.defaults or list())) - - config = dict() - for i, argument in enumerate(reversed(arguments)): - if i < len(defaults): - default = defaults[i] - if isinstance(default, Sized): - for j in range(len(default)): - config[f"{argument}_{j}"] = default_prior - continue - config[argument] = default_prior - - return path, config - - -def path_for_class(cls) -> List[str]: - """ - A list describing the import path for a given class. - - Parameters - ---------- - cls - A class with some module path - - Returns - ------- - A list of modules terminating in the name of a class - """ - return f"{cls.__module__}.{cls.__name__}".split(".") - - -# Sentinels for the per-instance lookup cache: a query can legitimately resolve to -# None, and the class-family probe in for_class_and_suffix_path relies on repeated -# expected misses, so both found-None and not-found must be cacheable. -_UNCACHED = object() -_NOT_FOUND = object() - - -class JSONPriorConfig: - def __init__(self, config_dict: dict, directory=None): - """ - Parses configuration describing priors associated with classes. - - The path pointing to a class is the same as the path to import it. - - Paths can be strings with '.' as a delimiter. - {"module.class": config} - - Else they can be a series of dictionary keys. - {"module": {"class": config}} - - Or any combination thereof. - - Parameters - ---------- - config_dict - A dictionary describing the prior configuration for constructor arguments - of different classes. - """ - self.obj = config_dict - self.directory = directory - self._path_value_map = None - self._path_value_tuples = None - self._lookup_cache = {} - - @property - def paths(self): - return list(self.path_value_map.keys()) - - @property - def path_value_map(self) -> dict: - """ - A dictionary matching every possible path to the configuration it points to. - """ - if self._path_value_map is None: - - def get_path_values(obj): - path_values = dict() - if isinstance(obj, dict): - for key, value in obj.items(): - path_values[key] = value - for path, path_value in get_path_values(value).items(): - path_values[f"{key}.{path}"] = path_value - - return path_values - - self._path_value_map = get_path_values(self.obj) - return self._path_value_map - - @property - def path_value_tuples(self) -> List[Tuple[str, object]]: - """ - Tuple pairs matching every possible path to the configuration it points to. - These are ordered by key length with the longest key first. - - Cached on the instance — this is consulted for every prior of every model - construction, and re-sorting the flattened configuration per lookup - dominated model deserialization. - """ - if self._path_value_tuples is None: - self._path_value_tuples = sorted( - list(self.path_value_map.items()), - key=lambda item: len(item[0]), - reverse=True, - ) - return self._path_value_tuples - - @classmethod - def from_directory(cls, directory: str) -> "JSONPriorConfig": - """ - Load JSONPriorConfiguration from a file. - - Parameters - ---------- - directory - The path to a file. - - Returns - ------- - A configuration instance. - """ - config_dict = dict() - - config_path = Path(directory) - - for suffix, parser in [ - ("json", json.load), - ("yaml", yaml.safe_load), - ("yml", yaml.safe_load), - ]: - for file in config_path.rglob(f"*.{suffix}"): - parts = file.relative_to(config_path).with_suffix("").parts - with open(file) as f: - config_dict[".".join(parts)] = parser(f) - - return JSONPriorConfig(config_dict, directory=directory) - - def __str__(self): - return json.dumps(self.obj) - - def __getitem__(self, item): - return JSONPriorConfig(self.obj[".".join(item)], directory=self.directory) - - def __contains__(self, item): - return ".".join(item) in self.obj - - def for_class_and_suffix_path(self, cls: Type, suffix_path: List[str]): - """ - Get configuration for a prior. - - If it is just basic configuration then the suffix path is just the - name of the prior in a list. Width configuration also adds an - additional "width_modifier" item. - - If configuration is not found for the class then configurations for - parents of the class are searched. - - Parameters - ---------- - cls - The class with which the prior is associated. - suffix_path - The path to the prior. - - Returns - ------- - A configuration dictionary - """ - for c in family(cls): - try: - return self(path_for_class(c) + suffix_path) - except KeyError: - pass - raise KeyError( - f"No config found for class {cls} and path {suffix_path} in {self.directory}" - ) - - def __call__(self, config_path: List[str]): - """ - Get the config at the end of the config_path. - - The configuration dictionary is traversed until config is found - at the end, else an exception is thrown. - - Parameters - ---------- - config_path - The import path of a package, module, class or class and constructor - argument name. - - Returns - ------- - A configuration dictionary or value - - Raises - ------ - PriorException - If no configuration is found. - """ - key = ".".join(config_path) - cached = self._lookup_cache.get(key, _UNCACHED) - if cached is _NOT_FOUND: - raise KeyError( - f"No configuration was found for the path {config_path}" - + ("" if self.directory is None else f" ({self.directory})") - ) - if cached is not _UNCACHED: - return cached - - for path, value in self.path_value_tuples: - if key.endswith(path): - self._lookup_cache[key] = value - return value - self._lookup_cache[key] = _NOT_FOUND - raise KeyError( - f"No configuration was found for the path {config_path}" - + ("" if self.directory is None else f" ({self.directory})") - ) +import inspect +import json +import logging +from collections.abc import Sized +from pathlib import Path +from typing import List, Type, Tuple + +import yaml + +from autonerves.directory_config import family + +logger = logging.getLogger(__name__) + +default_prior = { + "type": "Uniform", + "lower_limit": 0.0, + "upper_limit": 1.0, + "width_modifier": {"type": "Absolute", "value": 0.2}, + "limits": {"lower": 0.0, "upper": 1.0}, +} + + +def make_config_for_class(cls): + path = path_for_class(cls) + arg_spec = inspect.getfullargspec(cls) + arguments = arg_spec.args[1:] + defaults = list(reversed(arg_spec.defaults or list())) + + config = dict() + for i, argument in enumerate(reversed(arguments)): + if i < len(defaults): + default = defaults[i] + if isinstance(default, Sized): + for j in range(len(default)): + config[f"{argument}_{j}"] = default_prior + continue + config[argument] = default_prior + + return path, config + + +def path_for_class(cls) -> List[str]: + """ + A list describing the import path for a given class. + + Parameters + ---------- + cls + A class with some module path + + Returns + ------- + A list of modules terminating in the name of a class + """ + return f"{cls.__module__}.{cls.__name__}".split(".") + + +# Sentinels for the per-instance lookup cache: a query can legitimately resolve to +# None, and the class-family probe in for_class_and_suffix_path relies on repeated +# expected misses, so both found-None and not-found must be cacheable. +_UNCACHED = object() +_NOT_FOUND = object() + + +class JSONPriorConfig: + def __init__(self, config_dict: dict, directory=None): + """ + Parses configuration describing priors associated with classes. + + The path pointing to a class is the same as the path to import it. + + Paths can be strings with '.' as a delimiter. + {"module.class": config} + + Else they can be a series of dictionary keys. + {"module": {"class": config}} + + Or any combination thereof. + + Parameters + ---------- + config_dict + A dictionary describing the prior configuration for constructor arguments + of different classes. + """ + self.obj = config_dict + self.directory = directory + self._path_value_map = None + self._path_value_tuples = None + self._lookup_cache = {} + + @property + def paths(self): + return list(self.path_value_map.keys()) + + @property + def path_value_map(self) -> dict: + """ + A dictionary matching every possible path to the configuration it points to. + """ + if self._path_value_map is None: + + def get_path_values(obj): + path_values = dict() + if isinstance(obj, dict): + for key, value in obj.items(): + path_values[key] = value + for path, path_value in get_path_values(value).items(): + path_values[f"{key}.{path}"] = path_value + + return path_values + + self._path_value_map = get_path_values(self.obj) + return self._path_value_map + + @property + def path_value_tuples(self) -> List[Tuple[str, object]]: + """ + Tuple pairs matching every possible path to the configuration it points to. + These are ordered by key length with the longest key first. + + Cached on the instance — this is consulted for every prior of every model + construction, and re-sorting the flattened configuration per lookup + dominated model deserialization. + """ + if self._path_value_tuples is None: + self._path_value_tuples = sorted( + list(self.path_value_map.items()), + key=lambda item: len(item[0]), + reverse=True, + ) + return self._path_value_tuples + + @classmethod + def from_directory(cls, directory: str) -> "JSONPriorConfig": + """ + Load JSONPriorConfiguration from a file. + + Parameters + ---------- + directory + The path to a file. + + Returns + ------- + A configuration instance. + """ + config_dict = dict() + + config_path = Path(directory) + + for suffix, parser in [ + ("json", json.load), + ("yaml", yaml.safe_load), + ("yml", yaml.safe_load), + ]: + for file in config_path.rglob(f"*.{suffix}"): + parts = file.relative_to(config_path).with_suffix("").parts + with open(file) as f: + config_dict[".".join(parts)] = parser(f) + + return JSONPriorConfig(config_dict, directory=directory) + + def __str__(self): + return json.dumps(self.obj) + + def __getitem__(self, item): + return JSONPriorConfig(self.obj[".".join(item)], directory=self.directory) + + def __contains__(self, item): + return ".".join(item) in self.obj + + def for_class_and_suffix_path(self, cls: Type, suffix_path: List[str]): + """ + Get configuration for a prior. + + If it is just basic configuration then the suffix path is just the + name of the prior in a list. Width configuration also adds an + additional "width_modifier" item. + + If configuration is not found for the class then configurations for + parents of the class are searched. + + Parameters + ---------- + cls + The class with which the prior is associated. + suffix_path + The path to the prior. + + Returns + ------- + A configuration dictionary + """ + for c in family(cls): + try: + return self(path_for_class(c) + suffix_path) + except KeyError: + pass + raise KeyError( + f"No config found for class {cls} and path {suffix_path} in {self.directory}" + ) + + def __call__(self, config_path: List[str]): + """ + Get the config at the end of the config_path. + + The configuration dictionary is traversed until config is found + at the end, else an exception is thrown. + + Parameters + ---------- + config_path + The import path of a package, module, class or class and constructor + argument name. + + Returns + ------- + A configuration dictionary or value + + Raises + ------ + PriorException + If no configuration is found. + """ + key = ".".join(config_path) + cached = self._lookup_cache.get(key, _UNCACHED) + if cached is _NOT_FOUND: + raise KeyError( + f"No configuration was found for the path {config_path}" + + ("" if self.directory is None else f" ({self.directory})") + ) + if cached is not _UNCACHED: + return cached + + for path, value in self.path_value_tuples: + if key.endswith(path): + self._lookup_cache[key] = value + return value + self._lookup_cache[key] = _NOT_FOUND + raise KeyError( + f"No configuration was found for the path {config_path}" + + ("" if self.directory is None else f" ({self.directory})") + ) diff --git a/autonerves/mock/mock_real.py b/autonerves/mock/mock_real.py index 30c5075..67dbde8 100644 --- a/autonerves/mock/mock_real.py +++ b/autonerves/mock/mock_real.py @@ -1,61 +1,61 @@ -class Redshift: - def __init__(self, redshift=0.0): - self.redshift = redshift - - -class SphProfile: - def __init__(self, centre=(0.0, 0.0)): - """Generic circular profiles class to contain functions shared by light and - mass profiles. - - Parameters - ---------- - centre - The (y,x) coordinates of the origin of the profile. - """ - self.centre = centre - - -class EllProfile(SphProfile): - def __init__(self, centre=(0.0, 0.0), axis_ratio=1.0, phi=0.0): - """Generic elliptical profiles class to contain functions shared by light - and mass profiles. - - Parameters - ---------- - centre - The (y,x) coordinates of the origin of the profiles - axis_ratio - Ratio of profiles ellipse's minor and major axes (b/a) - phi - Rotational angle of profiles ellipse counter-clockwise from positive x-axis - """ - super(EllProfile, self).__init__(centre) - self.axis_ratio = axis_ratio - self.phi = phi - - -class Gaussian(EllProfile): - def __init__( - self, centre=(0.0, 0.0), axis_ratio=1.0, phi=0.0, intensity=0.1, sigma=0.01 - ): - """The elliptical Gaussian profile. - - Parameters - ---------- - centre - The (y,x) origin of the light profile. - axis_ratio - Ratio of light profiles ellipse's minor and major axes (b/a). - phi - Rotation angle of light profile counter-clockwise from positive x-axis. - intensity - Overall intensity normalisation of the light profiles (electrons per - second). - sigma - The full-width half-maximum of the Gaussian. - """ - super(Gaussian, self).__init__(centre, axis_ratio, phi) - - self.intensity = intensity - self.sigma = sigma +class Redshift: + def __init__(self, redshift=0.0): + self.redshift = redshift + + +class SphProfile: + def __init__(self, centre=(0.0, 0.0)): + """Generic circular profiles class to contain functions shared by light and + mass profiles. + + Parameters + ---------- + centre + The (y,x) coordinates of the origin of the profile. + """ + self.centre = centre + + +class EllProfile(SphProfile): + def __init__(self, centre=(0.0, 0.0), axis_ratio=1.0, phi=0.0): + """Generic elliptical profiles class to contain functions shared by light + and mass profiles. + + Parameters + ---------- + centre + The (y,x) coordinates of the origin of the profiles + axis_ratio + Ratio of profiles ellipse's minor and major axes (b/a) + phi + Rotational angle of profiles ellipse counter-clockwise from positive x-axis + """ + super(EllProfile, self).__init__(centre) + self.axis_ratio = axis_ratio + self.phi = phi + + +class Gaussian(EllProfile): + def __init__( + self, centre=(0.0, 0.0), axis_ratio=1.0, phi=0.0, intensity=0.1, sigma=0.01 + ): + """The elliptical Gaussian profile. + + Parameters + ---------- + centre + The (y,x) origin of the light profile. + axis_ratio + Ratio of light profiles ellipse's minor and major axes (b/a). + phi + Rotation angle of light profile counter-clockwise from positive x-axis. + intensity + Overall intensity normalisation of the light profiles (electrons per + second). + sigma + The full-width half-maximum of the Gaussian. + """ + super(Gaussian, self).__init__(centre, axis_ratio, phi) + + self.intensity = intensity + self.sigma = sigma diff --git a/autonerves/output.py b/autonerves/output.py index dfc8f4f..f6869fc 100644 --- a/autonerves/output.py +++ b/autonerves/output.py @@ -1,64 +1,64 @@ -from functools import wraps -from typing import Callable -import logging - -from autonerves.conf import instance - -logger = logging.getLogger(__name__) - - -def should_output(name: str) -> bool: - """ - Determine whether a file with a given name (excluding extension) should be output. - - This is configured in config/output.yaml. If the file is not present in the config, the default value is used. - - Parameters - ---------- - name - The name of the file to be output, excluding extension. - - Returns - ------- - Whether the file should be output. - """ - output_config = instance["output"] - try: - return output_config[name] - except KeyError: - return output_config["default"] - - -def conditional_output(func: Callable): - """ - Decorator for functions that output files. If the file should not be output, the function is not called. - - Parameters - ---------- - func - A method where the first argument is the name of the file to be output. - - Returns - ------- - The decorated function. - """ - - @wraps(func) - def wrapper(self, name: str, *args, **kwargs): - """ - Conditionally call the decorated function if the file should be output according - to the config. - - Parameters - ---------- - self - name - The name of the file to be output, excluding extension. - args - kwargs - """ - if should_output(name): - return func(self, name, *args, **kwargs) - logger.info(f"Skipping output of {name}") - - return wrapper +from functools import wraps +from typing import Callable +import logging + +from autonerves.conf import instance + +logger = logging.getLogger(__name__) + + +def should_output(name: str) -> bool: + """ + Determine whether a file with a given name (excluding extension) should be output. + + This is configured in config/output.yaml. If the file is not present in the config, the default value is used. + + Parameters + ---------- + name + The name of the file to be output, excluding extension. + + Returns + ------- + Whether the file should be output. + """ + output_config = instance["output"] + try: + return output_config[name] + except KeyError: + return output_config["default"] + + +def conditional_output(func: Callable): + """ + Decorator for functions that output files. If the file should not be output, the function is not called. + + Parameters + ---------- + func + A method where the first argument is the name of the file to be output. + + Returns + ------- + The decorated function. + """ + + @wraps(func) + def wrapper(self, name: str, *args, **kwargs): + """ + Conditionally call the decorated function if the file should be output according + to the config. + + Parameters + ---------- + self + name + The name of the file to be output, excluding extension. + args + kwargs + """ + if should_output(name): + return func(self, name, *args, **kwargs) + logger.info(f"Skipping output of {name}") + + return wrapper diff --git a/autonerves/tools/decorators.py b/autonerves/tools/decorators.py index ad73abc..035c05b 100644 --- a/autonerves/tools/decorators.py +++ b/autonerves/tools/decorators.py @@ -1,80 +1,80 @@ -import functools - -import numpy as np - - -class CachedProperty(object): - """ - A property that is only computed once per instance and then replaces - itself with an ordinary attribute. Deleting the attribute resets the - property. - - Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 - """ - - def __init__(self, func): - self.func = func - - def __get__(self, obj, cls): - if obj is None: - return self - if self.func.__name__ not in obj.__dict__: - obj.__dict__[self.func.__name__] = self.func(obj) - return obj.__dict__[self.func.__name__] - - -cached_property = CachedProperty - - -def cached_property_names(cls) -> frozenset: - """ - Return the names of every ``cached_property``-style descriptor declared - anywhere in ``cls``'s MRO. - - Recognises both stdlib :class:`functools.cached_property` and the - autonerves :class:`CachedProperty` wrapper above. Walks the MRO so - descriptors declared on base classes are picked up. - - The first call for a given class walks the MRO and caches the resulting - frozenset on the class itself under ``__cached_property_names_cache__``; - subsequent calls return the cached frozenset directly. The cache key is - stored under a dunder name so the result itself never appears in any - instance ``__dict__`` walk. - - Used by PyAutoFit and PyAutoArray to extend their existing - ``__dict__``-iteration filters with a forward-compat guard: any future - ``@cached_property`` declared on a model or Fit class will be - automatically excluded from instance construction, ``ModelInstance.dict``, - pickling, and JAX pytree flattening, preventing the class of bug that - PR PyAutoFit#1300 fixed for ``parameterization``. - - Parameters - ---------- - cls - The class to inspect. - - Returns - ------- - A frozenset of attribute names corresponding to ``cached_property`` or - autonerves ``CachedProperty`` descriptors found in ``cls.__mro__``. - """ - cache = cls.__dict__.get("__cached_property_names_cache__") - if cache is not None: - return cache - - names = set() - for base in cls.__mro__: - for attr_name, value in base.__dict__.items(): - if isinstance(value, (functools.cached_property, CachedProperty)): - names.add(attr_name) - - result = frozenset(names) - # Stash on the class itself (not a parent) so subclass overrides - # of cached_property descriptors are re-discovered on the subclass. - try: - setattr(cls, "__cached_property_names_cache__", result) - except (TypeError, AttributeError): - # Some classes (slotted, built-in) reject setattr; that's fine, - # the function still returns the correct value without memoisation. - pass - return result +import functools + +import numpy as np + + +class CachedProperty(object): + """ + A property that is only computed once per instance and then replaces + itself with an ordinary attribute. Deleting the attribute resets the + property. + + Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76 + """ + + def __init__(self, func): + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + if self.func.__name__ not in obj.__dict__: + obj.__dict__[self.func.__name__] = self.func(obj) + return obj.__dict__[self.func.__name__] + + +cached_property = CachedProperty + + +def cached_property_names(cls) -> frozenset: + """ + Return the names of every ``cached_property``-style descriptor declared + anywhere in ``cls``'s MRO. + + Recognises both stdlib :class:`functools.cached_property` and the + autonerves :class:`CachedProperty` wrapper above. Walks the MRO so + descriptors declared on base classes are picked up. + + The first call for a given class walks the MRO and caches the resulting + frozenset on the class itself under ``__cached_property_names_cache__``; + subsequent calls return the cached frozenset directly. The cache key is + stored under a dunder name so the result itself never appears in any + instance ``__dict__`` walk. + + Used by PyAutoFit and PyAutoArray to extend their existing + ``__dict__``-iteration filters with a forward-compat guard: any future + ``@cached_property`` declared on a model or Fit class will be + automatically excluded from instance construction, ``ModelInstance.dict``, + pickling, and JAX pytree flattening, preventing the class of bug that + PR PyAutoFit#1300 fixed for ``parameterization``. + + Parameters + ---------- + cls + The class to inspect. + + Returns + ------- + A frozenset of attribute names corresponding to ``cached_property`` or + autonerves ``CachedProperty`` descriptors found in ``cls.__mro__``. + """ + cache = cls.__dict__.get("__cached_property_names_cache__") + if cache is not None: + return cache + + names = set() + for base in cls.__mro__: + for attr_name, value in base.__dict__.items(): + if isinstance(value, (functools.cached_property, CachedProperty)): + names.add(attr_name) + + result = frozenset(names) + # Stash on the class itself (not a parent) so subclass overrides + # of cached_property descriptors are re-discovered on the subclass. + try: + setattr(cls, "__cached_property_names_cache__", result) + except (TypeError, AttributeError): + # Some classes (slotted, built-in) reject setattr; that's fine, + # the function still returns the correct value without memoisation. + pass + return result diff --git a/pyproject.toml b/pyproject.toml index 71a309b..5396575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,66 +1,66 @@ -[build-system] -requires = ["setuptools>=79.0", "setuptools-scm", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "autonerves" -dynamic = ["version"] -description = "PyAuto Configration" -readme = { file = "README.md", content-type = "text/markdown" } -license = { text = "MIT" } -requires-python = ">=3.9" -authors = [ - { name = "James Nightingale", email = "James.Nightingale@newcastle.ac.uk" }, - { name = "Richard Hayes", email = "richard@rghsoftware.co.uk" }, -] -classifiers = [ - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering :: Physics", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13" -] -keywords = ["cli"] -dependencies = [ - "pathlib", - "typing-inspect>=0.4.0", - "PyYAML>=6.0.1", - "numpy>=1.24.0,<3.0.0" -] - -[project.urls] -Homepage = "https://github.com/PyAutoLabs/PyAutoNerves" - -[tool.setuptools] -include-package-data = true - -[tool.setuptools.packages.find] -exclude = ["docs", "test_autonerves", "test_autonerves*"] - -[tool.setuptools_scm] -version_scheme = "post-release" -local_scheme = "no-local-version" - -[project.optional-dependencies] -jax = [ - "jax>=0.7.0,<0.11.0; python_version >= '3.11'", - "jaxlib>=0.7.0,<0.11.0; python_version >= '3.11'", - "jaxnnls==1.0.1; python_version >= '3.11'" -] -optional = [ - "autonerves[jax]", - "astropy>=5.0" -] -test = ["pytest"] -dev = ["pytest", "black"] - -[tool.pytest.ini_options] -testpaths = ["test_autonerves"] -filterwarnings = [ - "ignore:cuda_plugin_extension:UserWarning", - "ignore::DeprecationWarning:jax", +[build-system] +requires = ["setuptools>=79.0", "setuptools-scm", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "autonerves" +dynamic = ["version"] +description = "PyAuto Configration" +readme = { file = "README.md", content-type = "text/markdown" } +license = { text = "MIT" } +requires-python = ">=3.9" +authors = [ + { name = "James Nightingale", email = "James.Nightingale@newcastle.ac.uk" }, + { name = "Richard Hayes", email = "richard@rghsoftware.co.uk" }, +] +classifiers = [ + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Physics", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13" +] +keywords = ["cli"] +dependencies = [ + "pathlib", + "typing-inspect>=0.4.0", + "PyYAML>=6.0.1", + "numpy>=1.24.0,<3.0.0" +] + +[project.urls] +Homepage = "https://github.com/PyAutoLabs/PyAutoNerves" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +exclude = ["docs", "test_autonerves", "test_autonerves*"] + +[tool.setuptools_scm] +version_scheme = "post-release" +local_scheme = "no-local-version" + +[project.optional-dependencies] +jax = [ + "jax>=0.7.0,<0.11.0; python_version >= '3.11'", + "jaxlib>=0.7.0,<0.11.0; python_version >= '3.11'", + "jaxnnls==1.0.1; python_version >= '3.11'" +] +optional = [ + "autonerves[jax]", + "astropy>=5.0" +] +test = ["pytest"] +dev = ["pytest", "black"] + +[tool.pytest.ini_options] +testpaths = ["test_autonerves"] +filterwarnings = [ + "ignore:cuda_plugin_extension:UserWarning", + "ignore::DeprecationWarning:jax", ] \ No newline at end of file diff --git a/setup.py b/setup.py index 4da4962..467a065 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ -import os -from setuptools import setup - -version = os.environ.get("VERSION", "1.0.dev0") - -setup( - version=version, +import os +from setuptools import setup + +version = os.environ.get("VERSION", "1.0.dev0") + +setup( + version=version, ) \ No newline at end of file diff --git a/test_autonerves/conftest.py b/test_autonerves/conftest.py index f35aa3f..b52a728 100644 --- a/test_autonerves/conftest.py +++ b/test_autonerves/conftest.py @@ -1,26 +1,26 @@ -import pathlib - -import pytest - -from autonerves import conf - - -@pytest.fixture(scope="session", name="files_directory") -def make_files_directory(): - return pathlib.Path(__file__).parent / "files" - - -@pytest.fixture(scope="session", name="session_config") -def make_session_config(files_directory): - return conf.Config( - files_directory / "config", - files_directory / "default", - ) - - -@pytest.fixture(name="config") -def make_config(files_directory): - return conf.Config( - files_directory / "config", - files_directory / "default", - ) +import pathlib + +import pytest + +from autonerves import conf + + +@pytest.fixture(scope="session", name="files_directory") +def make_files_directory(): + return pathlib.Path(__file__).parent / "files" + + +@pytest.fixture(scope="session", name="session_config") +def make_session_config(files_directory): + return conf.Config( + files_directory / "config", + files_directory / "default", + ) + + +@pytest.fixture(name="config") +def make_config(files_directory): + return conf.Config( + files_directory / "config", + files_directory / "default", + ) diff --git a/test_autonerves/files/config/embedded.yaml b/test_autonerves/files/config/embedded.yaml index 9b93dd9..697fad1 100644 --- a/test_autonerves/files/config/embedded.yaml +++ b/test_autonerves/files/config/embedded.yaml @@ -1,4 +1,4 @@ -first: - first_a: - first_a_a: one - first_a_c: three +first: + first_a: + first_a_a: one + first_a_c: three diff --git a/test_autonerves/files/config/general.yaml b/test_autonerves/files/config/general.yaml index a9796e9..f9ed266 100644 --- a/test_autonerves/files/config/general.yaml +++ b/test_autonerves/files/config/general.yaml @@ -1,2 +1,2 @@ -hpc: +hpc: hpc_mode: false \ No newline at end of file diff --git a/test_autonerves/files/config/label.ini b/test_autonerves/files/config/label.ini index dcff1c9..f772989 100644 --- a/test_autonerves/files/config/label.ini +++ b/test_autonerves/files/config/label.ini @@ -1,9 +1,9 @@ -[label] -centre_0=x -centre_1=y -gamma=\gamma -contribution_factor=\omega0 -redshift=z - -[superscript] +[label] +centre_0=x +centre_1=y +gamma=\gamma +contribution_factor=\omega0 +redshift=z + +[superscript] EllProfile=l \ No newline at end of file diff --git a/test_autonerves/files/config/logging.yaml b/test_autonerves/files/config/logging.yaml index 6bd8722..0e803c6 100644 --- a/test_autonerves/files/config/logging.yaml +++ b/test_autonerves/files/config/logging.yaml @@ -1,12 +1,12 @@ -name: config -version: 1 -disable_existing_loggers: false -handlers: - console: - class: logging.StreamHandler - level: WARN - stream: ext://sys.stdout - -root: - level: INFO - handlers: [ console ] +name: config +version: 1 +disable_existing_loggers: false +handlers: + console: + class: logging.StreamHandler + level: WARN + stream: ext://sys.stdout + +root: + level: INFO + handlers: [ console ] diff --git a/test_autonerves/files/config/one/two.ini b/test_autonerves/files/config/one/two.ini index 727638f..2277aba 100644 --- a/test_autonerves/files/config/one/two.ini +++ b/test_autonerves/files/config/one/two.ini @@ -1,3 +1,3 @@ -[three] -four=five +[three] +four=five six=seven \ No newline at end of file diff --git a/test_autonerves/files/config/output.yaml b/test_autonerves/files/config/output.yaml index 1525237..1362a9c 100644 --- a/test_autonerves/files/config/output.yaml +++ b/test_autonerves/files/config/output.yaml @@ -1,3 +1,3 @@ -should_output: true -should_not_output: false +should_output: true +should_not_output: false default: true \ No newline at end of file diff --git a/test_autonerves/files/config/priors/mock_real.json b/test_autonerves/files/config/priors/mock_real.json index e01cab9..175e2d5 100644 --- a/test_autonerves/files/config/priors/mock_real.json +++ b/test_autonerves/files/config/priors/mock_real.json @@ -1,9 +1,9 @@ -{ - "Redshift": { - "redshift": { - "type": "Uniform", - "lower_limit": 0.0, - "upper_limit": 3.0 - } - } +{ + "Redshift": { + "redshift": { + "type": "Uniform", + "lower_limit": 0.0, + "upper_limit": 3.0 + } + } } \ No newline at end of file diff --git a/test_autonerves/files/config/priors/subdirectory/subconfig.yaml b/test_autonerves/files/config/priors/subdirectory/subconfig.yaml index de92798..0dbe2c4 100644 --- a/test_autonerves/files/config/priors/subdirectory/subconfig.yaml +++ b/test_autonerves/files/config/priors/subdirectory/subconfig.yaml @@ -1,5 +1,5 @@ -SubClass: - variable: - type: Uniform - lower_limit: 0.0 +SubClass: + variable: + type: Uniform + lower_limit: 0.0 upper_limit: 3.0 \ No newline at end of file diff --git a/test_autonerves/files/config/priors/test_yaml_config.yaml b/test_autonerves/files/config/priors/test_yaml_config.yaml index 5ffbad5..798c786 100644 --- a/test_autonerves/files/config/priors/test_yaml_config.yaml +++ b/test_autonerves/files/config/priors/test_yaml_config.yaml @@ -1,5 +1,5 @@ -YAMLClass: - variable: - type: Uniform - lower_limit: 0.0 +YAMLClass: + variable: + type: Uniform + lower_limit: 0.0 upper_limit: 3.0 \ No newline at end of file diff --git a/test_autonerves/files/config/text/label.ini b/test_autonerves/files/config/text/label.ini index 0663179..c962c63 100644 --- a/test_autonerves/files/config/text/label.ini +++ b/test_autonerves/files/config/text/label.ini @@ -1,4 +1,4 @@ -[label] - -[superscript] +[label] + +[superscript] Galaxy=g \ No newline at end of file diff --git a/test_autonerves/files/config_flip/general.yaml b/test_autonerves/files/config_flip/general.yaml index 459ca25..fa72799 100644 --- a/test_autonerves/files/config_flip/general.yaml +++ b/test_autonerves/files/config_flip/general.yaml @@ -1,2 +1,2 @@ -psf: +psf: use_fft_default: true # If True, PSFs are convolved using FFTs by default, which is faster and uses less memory in all cases except for very small PSFs, False uses direct convolution. \ No newline at end of file diff --git a/test_autonerves/files/default/default/other.ini b/test_autonerves/files/default/default/other.ini index 5408e70..90dcff1 100644 --- a/test_autonerves/files/default/default/other.ini +++ b/test_autonerves/files/default/default/other.ini @@ -1,2 +1,2 @@ -[section] +[section] key=value \ No newline at end of file diff --git a/test_autonerves/files/default/default_file.ini b/test_autonerves/files/default/default_file.ini index c8500ab..0dc1a63 100644 --- a/test_autonerves/files/default/default_file.ini +++ b/test_autonerves/files/default/default_file.ini @@ -1,2 +1,2 @@ -[section] +[section] key=file value \ No newline at end of file diff --git a/test_autonerves/files/default/embedded.yaml b/test_autonerves/files/default/embedded.yaml index 6e78077..6c56ed7 100644 --- a/test_autonerves/files/default/embedded.yaml +++ b/test_autonerves/files/default/embedded.yaml @@ -1,4 +1,4 @@ -first: - first_a: - first_a_a: one +first: + first_a: + first_a_a: one first_a_b: two \ No newline at end of file diff --git a/test_autonerves/files/default/general.ini b/test_autonerves/files/default/general.ini index 0a84003..9a333de 100644 --- a/test_autonerves/files/default/general.ini +++ b/test_autonerves/files/default/general.ini @@ -1,6 +1,6 @@ -[output] -identifier_version=4 - -[hpc] -hpc_mode=True +[output] +identifier_version=4 + +[hpc] +hpc_mode=True default_field=hello \ No newline at end of file diff --git a/test_autonerves/files/default/logging.yaml b/test_autonerves/files/default/logging.yaml index 2b5c6ba..8d93b7f 100644 --- a/test_autonerves/files/default/logging.yaml +++ b/test_autonerves/files/default/logging.yaml @@ -1,12 +1,12 @@ -name: default -version: 1 -disable_existing_loggers: false -handlers: - console: - class: logging.StreamHandler - level: WARN - stream: ext://sys.stdout - -root: - level: INFO - handlers: [ console ] +name: default +version: 1 +disable_existing_loggers: false +handlers: + console: + class: logging.StreamHandler + level: WARN + stream: ext://sys.stdout + +root: + level: INFO + handlers: [ console ] diff --git a/test_autonerves/files/default/one.yaml b/test_autonerves/files/default/one.yaml index 28c3c61..426842b 100644 --- a/test_autonerves/files/default/one.yaml +++ b/test_autonerves/files/default/one.yaml @@ -1,4 +1,4 @@ -two: - three: - four: five +two: + three: + four: five eight: nine \ No newline at end of file diff --git a/test_autonerves/files/default/priors/mock_real.json b/test_autonerves/files/default/priors/mock_real.json index 04eea7f..2ad9022 100644 --- a/test_autonerves/files/default/priors/mock_real.json +++ b/test_autonerves/files/default/priors/mock_real.json @@ -1,14 +1,14 @@ -{ - "Redshift": { - "redshift": { - "type": "Uniform", - "lower_limit": 0.0, - "upper_limit": 4.0 - }, - "rodshift": { - "type": "Uniform", - "lower_limit": 0.0, - "upper_limit": 4.0 - } - } +{ + "Redshift": { + "redshift": { + "type": "Uniform", + "lower_limit": 0.0, + "upper_limit": 4.0 + }, + "rodshift": { + "type": "Uniform", + "lower_limit": 0.0, + "upper_limit": 4.0 + } + } } \ No newline at end of file diff --git a/test_autonerves/files/default/text/label.ini b/test_autonerves/files/default/text/label.ini index 042cb3b..6ef0027 100644 --- a/test_autonerves/files/default/text/label.ini +++ b/test_autonerves/files/default/text/label.ini @@ -1,5 +1,5 @@ -[label] - -[superscript] -Galaxy=p +[label] + +[superscript] +Galaxy=p default_field=label default \ No newline at end of file diff --git a/test_autonerves/json_prior/source_code/module.py b/test_autonerves/json_prior/source_code/module.py index 447b3cb..f55c3de 100644 --- a/test_autonerves/json_prior/source_code/module.py +++ b/test_autonerves/json_prior/source_code/module.py @@ -1,4 +1,4 @@ -class MyClass: - def __init__(self, simple: float, tup=(1.0, 1.0)): - self.simple = simple - self.tup = tup +class MyClass: + def __init__(self, simple: float, tup=(1.0, 1.0)): + self.simple = simple + self.tup = tup diff --git a/test_autonerves/json_prior/source_code/subdirectory/subconfig.py b/test_autonerves/json_prior/source_code/subdirectory/subconfig.py index 15380e1..fcaf5ca 100644 --- a/test_autonerves/json_prior/source_code/subdirectory/subconfig.py +++ b/test_autonerves/json_prior/source_code/subdirectory/subconfig.py @@ -1,3 +1,3 @@ -class SubClass: - def __init__(self, variable: float): - self.variable = variable +class SubClass: + def __init__(self, variable: float): + self.variable = variable diff --git a/test_autonerves/json_prior/test_json_config.py b/test_autonerves/json_prior/test_json_config.py index d2284be..19cdc81 100644 --- a/test_autonerves/json_prior/test_json_config.py +++ b/test_autonerves/json_prior/test_json_config.py @@ -1,103 +1,103 @@ -import pytest - -import autonerves as aconf -from autonerves.mock.mock_real import SphProfile - - -@pytest.fixture(name="geometry_profile_path") -def make_geometry_profile_path(): - return ["autonerves", "mock", "mock_real", "SphProfile"] - - -def test_path_for_class(geometry_profile_path): - assert aconf.path_for_class(SphProfile) == geometry_profile_path - - -@pytest.mark.parametrize( - "config_dict, paths", - [ - ( - { - "autonerves.mock.mock_real.SphProfile": "test", - "autonerves.mock.mock_real.Other": "toast", - }, - ["autonerves.mock.mock_real.SphProfile", "autonerves.mock.mock_real.Other"], - ), - ( - {"autonerves.mock.mock_real": {"SphProfile": "test", "Other": "toast"}}, - [ - "autonerves.mock.mock_real", - "autonerves.mock.mock_real.SphProfile", - "autonerves.mock.mock_real.Other", - ], - ), - ( - { - "autonerves": { - "mock": {"mock_real": {"SphProfile": "test", "Other": "toast"}} - } - }, - [ - "autonerves", - "autonerves.mock", - "autonerves.mock.mock_real", - "autonerves.mock.mock_real.SphProfile", - "autonerves.mock.mock_real.Other", - ], - ), - ( - { - "autonerves": { - "mock": {"mock_real.SphProfile": "test", "mock_real.Other": "toast"} - } - }, - [ - "autonerves", - "autonerves.mock", - "autonerves.mock.mock_real.SphProfile", - "autonerves.mock.mock_real.Other", - ], - ), - ({"SphProfile": "test", "Other": "toast"}, ["SphProfile", "Other"]), - ( - {"mock_real.SphProfile": "test", "mock_real.Other": "toast"}, - ["mock_real.SphProfile", "mock_real.Other"], - ), - ( - {"mock_real": {"SphProfile": "test", "Other": "toast"}}, - ["mock_real", "mock_real.SphProfile", "mock_real.Other"], - ), - ], -) -def test_paths(config_dict, paths): - config = aconf.JSONPriorConfig(config_dict) - assert config.paths == paths - - -@pytest.mark.parametrize( - "config_dict", - [ - { - "autonerves.mock.mock_real.SphProfile": "test", - "autonerves.mock.mock_real.Other": "toast", - }, - {"autonerves.mock.mock_real": {"SphProfile": "test", "Other": "toast"}}, - {"autonerves": {"mock": {"mock_real": {"SphProfile": "test", "Other": "toast"}}}}, - { - "autonerves": { - "mock": {"mock_real.SphProfile": "test", "mock_real.Other": "toast"} - } - }, - {"SphProfile": "test", "Other": "toast"}, - {"mock_real": {"SphProfile": "test", "Other": "toast"}}, - ], -) -def test_config_for_path(geometry_profile_path, config_dict): - config = aconf.JSONPriorConfig(config_dict) - assert config(geometry_profile_path) == "test" - assert config(["autonerves", "mock", "mock_real", "Other"]) == "toast" - - -def test_path_double(): - config = aconf.JSONPriorConfig({"mock_real": {"SphProfile": "test"}}) - assert config(["something", "mock_real", "mock_real", "SphProfile"]) == "test" +import pytest + +import autonerves as aconf +from autonerves.mock.mock_real import SphProfile + + +@pytest.fixture(name="geometry_profile_path") +def make_geometry_profile_path(): + return ["autonerves", "mock", "mock_real", "SphProfile"] + + +def test_path_for_class(geometry_profile_path): + assert aconf.path_for_class(SphProfile) == geometry_profile_path + + +@pytest.mark.parametrize( + "config_dict, paths", + [ + ( + { + "autonerves.mock.mock_real.SphProfile": "test", + "autonerves.mock.mock_real.Other": "toast", + }, + ["autonerves.mock.mock_real.SphProfile", "autonerves.mock.mock_real.Other"], + ), + ( + {"autonerves.mock.mock_real": {"SphProfile": "test", "Other": "toast"}}, + [ + "autonerves.mock.mock_real", + "autonerves.mock.mock_real.SphProfile", + "autonerves.mock.mock_real.Other", + ], + ), + ( + { + "autonerves": { + "mock": {"mock_real": {"SphProfile": "test", "Other": "toast"}} + } + }, + [ + "autonerves", + "autonerves.mock", + "autonerves.mock.mock_real", + "autonerves.mock.mock_real.SphProfile", + "autonerves.mock.mock_real.Other", + ], + ), + ( + { + "autonerves": { + "mock": {"mock_real.SphProfile": "test", "mock_real.Other": "toast"} + } + }, + [ + "autonerves", + "autonerves.mock", + "autonerves.mock.mock_real.SphProfile", + "autonerves.mock.mock_real.Other", + ], + ), + ({"SphProfile": "test", "Other": "toast"}, ["SphProfile", "Other"]), + ( + {"mock_real.SphProfile": "test", "mock_real.Other": "toast"}, + ["mock_real.SphProfile", "mock_real.Other"], + ), + ( + {"mock_real": {"SphProfile": "test", "Other": "toast"}}, + ["mock_real", "mock_real.SphProfile", "mock_real.Other"], + ), + ], +) +def test_paths(config_dict, paths): + config = aconf.JSONPriorConfig(config_dict) + assert config.paths == paths + + +@pytest.mark.parametrize( + "config_dict", + [ + { + "autonerves.mock.mock_real.SphProfile": "test", + "autonerves.mock.mock_real.Other": "toast", + }, + {"autonerves.mock.mock_real": {"SphProfile": "test", "Other": "toast"}}, + {"autonerves": {"mock": {"mock_real": {"SphProfile": "test", "Other": "toast"}}}}, + { + "autonerves": { + "mock": {"mock_real.SphProfile": "test", "mock_real.Other": "toast"} + } + }, + {"SphProfile": "test", "Other": "toast"}, + {"mock_real": {"SphProfile": "test", "Other": "toast"}}, + ], +) +def test_config_for_path(geometry_profile_path, config_dict): + config = aconf.JSONPriorConfig(config_dict) + assert config(geometry_profile_path) == "test" + assert config(["autonerves", "mock", "mock_real", "Other"]) == "toast" + + +def test_path_double(): + config = aconf.JSONPriorConfig({"mock_real": {"SphProfile": "test"}}) + assert config(["something", "mock_real", "mock_real", "SphProfile"]) == "test" diff --git a/test_autonerves/json_prior/test_yaml_config.py b/test_autonerves/json_prior/test_yaml_config.py index fb8994a..cbbc20e 100644 --- a/test_autonerves/json_prior/test_yaml_config.py +++ b/test_autonerves/json_prior/test_yaml_config.py @@ -1,27 +1,27 @@ -from .source_code.subdirectory.subconfig import SubClass - - -class YAMLClass: - def __init__(self, variable: float): - self.variable = variable - - -def test_load_yaml_config(config): - assert config.prior_config.for_class_and_suffix_path(YAMLClass, ["variable"]) == { - "lower_limit": 0.0, - "type": "Uniform", - "upper_limit": 3.0, - } - - -def test_embedded_path(config): - path_value_map = config.prior_config.prior_configs[0].path_value_map - assert "subdirectory.subconfig.SubClass.variable.type" in path_value_map - - -def test_subdirectory(config): - assert config.prior_config.for_class_and_suffix_path(SubClass, ["variable"]) == { - "lower_limit": 0.0, - "type": "Uniform", - "upper_limit": 3.0, - } +from .source_code.subdirectory.subconfig import SubClass + + +class YAMLClass: + def __init__(self, variable: float): + self.variable = variable + + +def test_load_yaml_config(config): + assert config.prior_config.for_class_and_suffix_path(YAMLClass, ["variable"]) == { + "lower_limit": 0.0, + "type": "Uniform", + "upper_limit": 3.0, + } + + +def test_embedded_path(config): + path_value_map = config.prior_config.prior_configs[0].path_value_map + assert "subdirectory.subconfig.SubClass.variable.type" in path_value_map + + +def test_subdirectory(config): + assert config.prior_config.for_class_and_suffix_path(SubClass, ["variable"]) == { + "lower_limit": 0.0, + "type": "Uniform", + "upper_limit": 3.0, + } diff --git a/test_autonerves/test_config.py b/test_autonerves/test_config.py index 9414e12..a158a0e 100644 --- a/test_autonerves/test_config.py +++ b/test_autonerves/test_config.py @@ -1,56 +1,56 @@ -from pathlib import Path - -import pytest - -from autonerves import conf -from autonerves.directory_config import NamedConfig -from autonerves.mock.mock_real import EllProfile, Gaussian -from autonerves.exc import ConfigException - -directory = Path(__file__).resolve().parent - - -class MockClass: - pass - - -@pytest.fixture(name="label_config") -def make_label_config(): - return NamedConfig(f"{directory}/files/config/label.ini") - - -class TestLabel: - def test_basic(self, label_config): - assert label_config["label"]["centre_0"] == "x" - assert label_config["label"]["redshift"] == "z" - - def test_escaped(self, label_config): - assert label_config["label"]["gamma"] == r"\gamma" - assert label_config["label"]["contribution_factor"] == r"\omega0" - - def test_superscript(self, label_config): - assert label_config["superscript"].family(EllProfile) == "l" - - def test_inheritance(self, label_config): - assert label_config["superscript"].family(Gaussian) == "l" - - def test_exception(self, label_config): - with pytest.raises(KeyError): - label_config["superscript"].family(MockClass) - - -@pytest.fixture(name="config") -def make_config(): - return conf.Config() - - -def test_path_does_not_exist(config, tmp_path): - with pytest.raises(ConfigException): - config.push(str(tmp_path / "does_not_exist")) - - -def test_path_empty(config, tmp_path): - empty = tmp_path / "empty" - empty.mkdir() - with pytest.raises(ConfigException): - config.push(str(empty)) +from pathlib import Path + +import pytest + +from autonerves import conf +from autonerves.directory_config import NamedConfig +from autonerves.mock.mock_real import EllProfile, Gaussian +from autonerves.exc import ConfigException + +directory = Path(__file__).resolve().parent + + +class MockClass: + pass + + +@pytest.fixture(name="label_config") +def make_label_config(): + return NamedConfig(f"{directory}/files/config/label.ini") + + +class TestLabel: + def test_basic(self, label_config): + assert label_config["label"]["centre_0"] == "x" + assert label_config["label"]["redshift"] == "z" + + def test_escaped(self, label_config): + assert label_config["label"]["gamma"] == r"\gamma" + assert label_config["label"]["contribution_factor"] == r"\omega0" + + def test_superscript(self, label_config): + assert label_config["superscript"].family(EllProfile) == "l" + + def test_inheritance(self, label_config): + assert label_config["superscript"].family(Gaussian) == "l" + + def test_exception(self, label_config): + with pytest.raises(KeyError): + label_config["superscript"].family(MockClass) + + +@pytest.fixture(name="config") +def make_config(): + return conf.Config() + + +def test_path_does_not_exist(config, tmp_path): + with pytest.raises(ConfigException): + config.push(str(tmp_path / "does_not_exist")) + + +def test_path_empty(config, tmp_path): + empty = tmp_path / "empty" + empty.mkdir() + with pytest.raises(ConfigException): + config.push(str(empty)) diff --git a/test_autonerves/test_decorator.py b/test_autonerves/test_decorator.py index d90f530..2495e43 100644 --- a/test_autonerves/test_decorator.py +++ b/test_autonerves/test_decorator.py @@ -1,19 +1,19 @@ -import pytest - -from autonerves import conf -from autonerves.conf import with_config - - -@pytest.fixture(autouse=True) -def push_configs(files_directory): - conf.instance.push(files_directory / "config") - conf.instance.push(files_directory / "default") - - -@with_config("general", "output", "identifier_version", value=9) -def test_with_config(): - assert conf.instance["general"]["output"]["identifier_version"] == 9 - - -def test_config(): - assert conf.instance["general"]["output"]["identifier_version"] == 4 +import pytest + +from autonerves import conf +from autonerves.conf import with_config + + +@pytest.fixture(autouse=True) +def push_configs(files_directory): + conf.instance.push(files_directory / "config") + conf.instance.push(files_directory / "default") + + +@with_config("general", "output", "identifier_version", value=9) +def test_with_config(): + assert conf.instance["general"]["output"]["identifier_version"] == 9 + + +def test_config(): + assert conf.instance["general"]["output"]["identifier_version"] == 4 diff --git a/test_autonerves/test_default.py b/test_autonerves/test_default.py index 27e9049..2ab0621 100644 --- a/test_autonerves/test_default.py +++ b/test_autonerves/test_default.py @@ -1,88 +1,88 @@ -from autonerves.mock.mock_real import Redshift - - -def test_override_file(session_config): - hpc = session_config["general"]["hpc"] - - assert hpc["hpc_mode"] is False - assert hpc["default_field"] == "hello" - - -def test_logging_config(config, files_directory): - assert config.logging_config["name"] == "config" - - config.push(files_directory / "default") - assert config.logging_config["name"] == "default" - - -def test_push(config, files_directory): - assert len(config.configs) == 2 - assert config["general"]["hpc"]["hpc_mode"] is False - - config.push(files_directory / "default") - - assert len(config.configs) == 2 - assert config["general"]["hpc"]["hpc_mode"] is True - - config.push(files_directory / "config") - - assert len(config.configs) == 2 - assert config["general"]["hpc"]["hpc_mode"] is False - - -def test_keep_first(config, files_directory): - config.push(files_directory / "default", keep_first=True) - - assert config["general"]["hpc"]["hpc_mode"] is False - - -def test_override_in_directory(session_config): - superscript = session_config["text"]["label"]["superscript"] - - assert superscript["Galaxy"] == "g" - assert superscript["default_field"] == "label default" - - -def test_novel_directory(session_config): - assert session_config["default"]["other"]["section"]["key"] == "value" - - -def test_novel_file(session_config): - assert session_config["default_file"]["section"]["key"] == "file value" - - -def test_json(session_config): - assert ( - session_config.prior_config.for_class_and_suffix_path(Redshift, ["redshift"])[ - "upper_limit" - ] - == 3.0 - ) - assert ( - session_config.prior_config.for_class_and_suffix_path(Redshift, ["rodshift"])[ - "upper_limit" - ] - == 4.0 - ) - - -def test_embedded_yaml_default(session_config): - embedded_dict = session_config["embedded"]["first"]["first_a"] - - assert embedded_dict["first_a_a"] == "one" - assert embedded_dict["first_a_b"] == "two" - assert embedded_dict["first_a_c"] == "three" - - -def test_as_dict(session_config): - embedded_dict = session_config["embedded"]["first"]["first_a"] - - assert {**embedded_dict} - - -def test_mix_files(session_config): - embedded_dict = session_config["one"]["two"]["three"] - - assert embedded_dict["four"] == "five" - assert embedded_dict["six"] == "seven" - assert embedded_dict["eight"] == "nine" +from autonerves.mock.mock_real import Redshift + + +def test_override_file(session_config): + hpc = session_config["general"]["hpc"] + + assert hpc["hpc_mode"] is False + assert hpc["default_field"] == "hello" + + +def test_logging_config(config, files_directory): + assert config.logging_config["name"] == "config" + + config.push(files_directory / "default") + assert config.logging_config["name"] == "default" + + +def test_push(config, files_directory): + assert len(config.configs) == 2 + assert config["general"]["hpc"]["hpc_mode"] is False + + config.push(files_directory / "default") + + assert len(config.configs) == 2 + assert config["general"]["hpc"]["hpc_mode"] is True + + config.push(files_directory / "config") + + assert len(config.configs) == 2 + assert config["general"]["hpc"]["hpc_mode"] is False + + +def test_keep_first(config, files_directory): + config.push(files_directory / "default", keep_first=True) + + assert config["general"]["hpc"]["hpc_mode"] is False + + +def test_override_in_directory(session_config): + superscript = session_config["text"]["label"]["superscript"] + + assert superscript["Galaxy"] == "g" + assert superscript["default_field"] == "label default" + + +def test_novel_directory(session_config): + assert session_config["default"]["other"]["section"]["key"] == "value" + + +def test_novel_file(session_config): + assert session_config["default_file"]["section"]["key"] == "file value" + + +def test_json(session_config): + assert ( + session_config.prior_config.for_class_and_suffix_path(Redshift, ["redshift"])[ + "upper_limit" + ] + == 3.0 + ) + assert ( + session_config.prior_config.for_class_and_suffix_path(Redshift, ["rodshift"])[ + "upper_limit" + ] + == 4.0 + ) + + +def test_embedded_yaml_default(session_config): + embedded_dict = session_config["embedded"]["first"]["first_a"] + + assert embedded_dict["first_a_a"] == "one" + assert embedded_dict["first_a_b"] == "two" + assert embedded_dict["first_a_c"] == "three" + + +def test_as_dict(session_config): + embedded_dict = session_config["embedded"]["first"]["first_a"] + + assert {**embedded_dict} + + +def test_mix_files(session_config): + embedded_dict = session_config["one"]["two"]["three"] + + assert embedded_dict["four"] == "five" + assert embedded_dict["six"] == "seven" + assert embedded_dict["eight"] == "nine" diff --git a/test_autonerves/test_fitsable.py b/test_autonerves/test_fitsable.py index 51be151..9a135a2 100644 --- a/test_autonerves/test_fitsable.py +++ b/test_autonerves/test_fitsable.py @@ -1,79 +1,79 @@ -import pytest - -from astropy.io import fits -import numpy as np -import os -from pathlib import Path - -from autonerves import conf -from autonerves import fitsable - -test_path = Path(__file__).resolve().parent - -test_data_path = Path(__file__).resolve().parent / "files" - - -def create_fits(fits_path, array): - fits_path = Path(fits_path) - if fits_path.exists(): - os.remove(fits_path) - - hdu_list = fits.HDUList() - - hdu_list.append(fits.ImageHDU(array)) - - hdu_list.writeto(f"{fits_path}") - - - -def test__ndarray_via_fits_from(): - arr = fitsable.ndarray_via_fits_from( - file_path=test_data_path / "3x3_ones.fits", hdu=0 - ) - - assert (arr == np.ones((3, 3))).all() - - arr = fitsable.ndarray_via_fits_from( - file_path=test_data_path / "4x3_ones.fits", hdu=0 - ) - - assert (arr == np.ones((4, 3))).all() - - -def test__output_to_fits(): - file_path = test_data_path / "array_out.fits" - - if file_path.exists(): - os.remove(file_path) - - arr = np.array([[10.0, 30.0, 40.0], [92.0, 19.0, 20.0]]) - - fitsable.output_to_fits(arr, file_path=file_path) - - array_load = fitsable.ndarray_via_fits_from(file_path=file_path, hdu=0) - - assert (arr == array_load).all() - - -def test__output_to_fits__header_dict(): - file_path = test_data_path / "array_out.fits" - - if file_path.exists(): - os.remove(file_path) - - arr = np.array([[10.0, 30.0, 40.0], [92.0, 19.0, 20.0]]) - - fitsable.output_to_fits(arr, file_path=file_path, header_dict={"A": 1}) - - header = fitsable.header_obj_from(file_path=file_path, hdu=0) - - assert header["A"] == 1 - - -def test__header_obj_from(): - header_obj = fitsable.header_obj_from( - file_path=test_data_path / "3x3_ones.fits", hdu=0 - ) - - assert isinstance(header_obj, fits.header.Header) - assert header_obj["BITPIX"] == -64 +import pytest + +from astropy.io import fits +import numpy as np +import os +from pathlib import Path + +from autonerves import conf +from autonerves import fitsable + +test_path = Path(__file__).resolve().parent + +test_data_path = Path(__file__).resolve().parent / "files" + + +def create_fits(fits_path, array): + fits_path = Path(fits_path) + if fits_path.exists(): + os.remove(fits_path) + + hdu_list = fits.HDUList() + + hdu_list.append(fits.ImageHDU(array)) + + hdu_list.writeto(f"{fits_path}") + + + +def test__ndarray_via_fits_from(): + arr = fitsable.ndarray_via_fits_from( + file_path=test_data_path / "3x3_ones.fits", hdu=0 + ) + + assert (arr == np.ones((3, 3))).all() + + arr = fitsable.ndarray_via_fits_from( + file_path=test_data_path / "4x3_ones.fits", hdu=0 + ) + + assert (arr == np.ones((4, 3))).all() + + +def test__output_to_fits(): + file_path = test_data_path / "array_out.fits" + + if file_path.exists(): + os.remove(file_path) + + arr = np.array([[10.0, 30.0, 40.0], [92.0, 19.0, 20.0]]) + + fitsable.output_to_fits(arr, file_path=file_path) + + array_load = fitsable.ndarray_via_fits_from(file_path=file_path, hdu=0) + + assert (arr == array_load).all() + + +def test__output_to_fits__header_dict(): + file_path = test_data_path / "array_out.fits" + + if file_path.exists(): + os.remove(file_path) + + arr = np.array([[10.0, 30.0, 40.0], [92.0, 19.0, 20.0]]) + + fitsable.output_to_fits(arr, file_path=file_path, header_dict={"A": 1}) + + header = fitsable.header_obj_from(file_path=file_path, hdu=0) + + assert header["A"] == 1 + + +def test__header_obj_from(): + header_obj = fitsable.header_obj_from( + file_path=test_data_path / "3x3_ones.fits", hdu=0 + ) + + assert isinstance(header_obj, fits.header.Header) + assert header_obj["BITPIX"] == -64 diff --git a/test_autonerves/test_output_config.py b/test_autonerves/test_output_config.py index a87f4a9..494b5fc 100644 --- a/test_autonerves/test_output_config.py +++ b/test_autonerves/test_output_config.py @@ -1,46 +1,46 @@ -import pytest - -from autonerves import instance -from autonerves.conf import with_config -from autonerves.output import conditional_output - - -class OutputClass: - def __init__(self): - self.output_names = [] - - @conditional_output - def output_function(self, name): - self.output_names.append(name) - - -@pytest.fixture(name="output_class") -def make_output_class(): - return OutputClass() - - -@pytest.fixture(autouse=True) -def add_config(files_directory): - instance.push(files_directory / "config") - - -def test_output(output_class): - output_class.output_function("should_output") - assert output_class.output_names == ["should_output"] - - -def test_no_output(output_class): - output_class.output_function("should_not_output") - assert output_class.output_names == [] - - -@with_config("output", "default", value=True) -def test_default_true(output_class): - output_class.output_function("other") - assert output_class.output_names == ["other"] - - -@with_config("output", "default", value=False) -def test_default_false(output_class): - output_class.output_function("other") - assert output_class.output_names == [] +import pytest + +from autonerves import instance +from autonerves.conf import with_config +from autonerves.output import conditional_output + + +class OutputClass: + def __init__(self): + self.output_names = [] + + @conditional_output + def output_function(self, name): + self.output_names.append(name) + + +@pytest.fixture(name="output_class") +def make_output_class(): + return OutputClass() + + +@pytest.fixture(autouse=True) +def add_config(files_directory): + instance.push(files_directory / "config") + + +def test_output(output_class): + output_class.output_function("should_output") + assert output_class.output_names == ["should_output"] + + +def test_no_output(output_class): + output_class.output_function("should_not_output") + assert output_class.output_names == [] + + +@with_config("output", "default", value=True) +def test_default_true(output_class): + output_class.output_function("other") + assert output_class.output_names == ["other"] + + +@with_config("output", "default", value=False) +def test_default_false(output_class): + output_class.output_function("other") + assert output_class.output_names == [] diff --git a/test_autonerves/tools/test_decorators.py b/test_autonerves/tools/test_decorators.py index c9877ca..4995731 100644 --- a/test_autonerves/tools/test_decorators.py +++ b/test_autonerves/tools/test_decorators.py @@ -1,17 +1,17 @@ -from autonerves.tools.decorators import cached_property - - -class MockClass: - def __init__(self, value): - self._value = value - - @cached_property - def value(self): - return self._value - - -def test__profile_decorator_times_decorated_function(files_directory): - cls = MockClass(value=1.0) - cls.value - - assert cls.__dict__["value"] == 1.0 +from autonerves.tools.decorators import cached_property + + +class MockClass: + def __init__(self, value): + self._value = value + + @cached_property + def value(self): + return self._value + + +def test__profile_decorator_times_decorated_function(files_directory): + cls = MockClass(value=1.0) + cls.value + + assert cls.__dict__["value"] == 1.0