energyplus-toolkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. energyplus_toolkit-0.1.0/PKG-INFO +148 -0
  2. energyplus_toolkit-0.1.0/README.md +116 -0
  3. energyplus_toolkit-0.1.0/energyplus_toolkit/__init__.py +5 -0
  4. energyplus_toolkit-0.1.0/energyplus_toolkit/cli.py +150 -0
  5. energyplus_toolkit-0.1.0/energyplus_toolkit/format/__init__.py +1 -0
  6. energyplus_toolkit-0.1.0/energyplus_toolkit/format/cpp/__init__.py +0 -0
  7. energyplus_toolkit-0.1.0/energyplus_toolkit/format/cpp/unit_test_file.py +666 -0
  8. energyplus_toolkit-0.1.0/energyplus_toolkit/format/runner.py +257 -0
  9. energyplus_toolkit-0.1.0/energyplus_toolkit/input/__init__.py +4 -0
  10. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/__init__.py +0 -0
  11. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/idd_constants.py +25 -0
  12. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/idd_field.py +549 -0
  13. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/idd_file.py +182 -0
  14. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/idd_object.py +376 -0
  15. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idd/idd_regex.py +80 -0
  16. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/__init__.py +0 -0
  17. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/idf_constants.py +48 -0
  18. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/idf_field.py +435 -0
  19. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/idf_file.py +278 -0
  20. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/idf_object.py +507 -0
  21. energyplus_toolkit-0.1.0/energyplus_toolkit/input/idf/idf_regex.py +48 -0
  22. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/PKG-INFO +148 -0
  23. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/SOURCES.txt +29 -0
  24. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/dependency_links.txt +1 -0
  25. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/entry_points.txt +2 -0
  26. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/requires.txt +14 -0
  27. energyplus_toolkit-0.1.0/energyplus_toolkit.egg-info/top_level.txt +1 -0
  28. energyplus_toolkit-0.1.0/pyproject.toml +89 -0
  29. energyplus_toolkit-0.1.0/setup.cfg +4 -0
  30. energyplus_toolkit-0.1.0/tests/test_cli.py +193 -0
  31. energyplus_toolkit-0.1.0/tests/test_layering.py +30 -0
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: energyplus-toolkit
3
+ Version: 0.1.0
4
+ Summary: Tools for EnergyPlus input files: parse, write back and reformat IDD and IDF files, and the IDF in EnergyPlus C++ unit tests.
5
+ Author-email: Julien Marrec <contact@effibem.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://effibem.com
8
+ Project-URL: Repository, https://github.com/jmarrec/energyplus-toolkit
9
+ Project-URL: Issues, https://github.com/jmarrec/energyplus-toolkit/issues
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest>=9.0.2; extra == "test"
22
+ Requires-Dist: pytest-cov>=7.0.0; extra == "test"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pre-commit>=4.5.0; extra == "dev"
25
+ Requires-Dist: twine>=6.2.0; extra == "dev"
26
+ Requires-Dist: bump2version>=1.0.1; extra == "dev"
27
+ Requires-Dist: mypy>=1.19.0; extra == "dev"
28
+ Requires-Dist: flake8>=7.3.0; extra == "dev"
29
+ Requires-Dist: flake8-docstrings>=1.7.0; extra == "dev"
30
+ Requires-Dist: black>=25.12.0; extra == "dev"
31
+ Requires-Dist: isort>=7.0.0; extra == "dev"
32
+
33
+ # energyplus-toolkit
34
+
35
+ [![pypi](https://img.shields.io/pypi/v/energyplus-toolkit.svg)](https://pypi.org/project/energyplus-toolkit/)
36
+ [![python](https://img.shields.io/pypi/pyversions/energyplus-toolkit.svg)](https://pypi.org/project/energyplus-toolkit/)
37
+ [![Build Status](https://github.com/jmarrec/energyplus-toolkit/actions/workflows/dev.yml/badge.svg)](https://github.com/jmarrec/energyplus-toolkit/actions/workflows/dev.yml)
38
+
39
+ Tools for [EnergyPlus](https://energyplus.net) input files:
40
+
41
+ - parse IDD and IDF files, link each IDF object and field to its IDD definition, and write them back faithfully;
42
+ - reformat IDF and IDD files, and the IDF embedded in EnergyPlus C++ unit tests, from the command line or as
43
+ pre-commit hooks.
44
+
45
+ * GitHub: <https://github.com/jmarrec/energyplus-toolkit>
46
+ * PyPI: <https://pypi.org/project/energyplus-toolkit/>
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install energyplus-toolkit
52
+ ```
53
+
54
+ Requires Python 3.11+.
55
+
56
+ For development:
57
+
58
+ ```bash
59
+ pip install -e ".[dev,test]" # With the dev tools (pre-commit, black, isort, flake8, mypy) and the tests (pytest)
60
+ pre-commit install
61
+ ```
62
+
63
+ ## Command line
64
+
65
+ ```shell
66
+ # Reformat in place, like black: files and/or folders (searched recursively), in parallel
67
+ energyplus-toolkit format idf --idd Energy+.idd testfiles/
68
+ energyplus-toolkit format idd Energy+.idd
69
+ energyplus-toolkit format unit-test --idd Energy+.idd tst/EnergyPlus/unit
70
+
71
+ # Only look: list the files that would change (exit 1 if any), or print a diff
72
+ energyplus-toolkit format idf --idd Energy+.idd --check testfiles/
73
+ energyplus-toolkit format idf --idd Energy+.idd --diff testfiles/1ZoneUncontrolled.idf
74
+ ```
75
+
76
+ Options (see `--help`): `--exclude REGEX`, `-j N` workers, `-q` quiet, `-o OUTPUT` (single file, `-` for stdout),
77
+ and for IDF / unit tests `--leading N` (spaces before object names) and `--keep-integer-reals` (keep `8` instead of
78
+ writing `8.0` in a real field).
79
+
80
+ Exit codes: 0 when done, 1 if `--check` found files to reformat, 123 on error.
81
+
82
+ Content is never lost: a file or block that can't be reformatted safely (unknown object, text outside a field, IMF or
83
+ parametric file, C++ code building the IDF...) is left unchanged, with a warning.
84
+
85
+ ## pre-commit hooks
86
+
87
+ ```yaml
88
+ - repo: https://github.com/jmarrec/energyplus-toolkit
89
+ rev: v0.1.0
90
+ hooks:
91
+ - id: energyplus-format-idd
92
+ args: [--idd, idd/Energy+.idd.in]
93
+ - id: energyplus-format-idf
94
+ args: [--idd, idd/Energy+.idd.in, -q]
95
+ - id: energyplus-format-unit-test
96
+ args: [--idd, idd/Energy+.idd.in, -q]
97
+ ```
98
+
99
+ Each hook needs the IDD to use (`--idd`). For unit tests, put the hook before clang-format: reformatting then
100
+ clang-format is stable.
101
+
102
+ ## Library
103
+
104
+ The IDD gives each IDF object and field its definition: type, units, default, choices, extensible groups...
105
+
106
+ ```python
107
+ from pathlib import Path
108
+
109
+ from energyplus_toolkit.input.idd.idd_file import IddFile
110
+ from energyplus_toolkit.input.idf.idf_file import IdfFile
111
+ from energyplus_toolkit.input.idf.idf_object import IdfObject
112
+
113
+ idd = IddFile.load(Path("Energy+.idd"))
114
+
115
+ # A whole IDF file: each object is linked to its IDD object
116
+ idf = IdfFile.load("in.idf", idd) # or IdfFile.loads(text, idd)
117
+ for zone in idf.get_objects_by_type("Zone"):
118
+ print(zone.name, zone.getDouble(1), zone.getDouble(1, return_default=True)) # "Z2 None 0.0" if left blank
119
+ idf.save("out.idf", overwrite=True) # or str(idf)
120
+
121
+ # A single object, from its text and its IDD object
122
+ zone = IdfObject.load(idd_object=idd.getObject("Zone"), text="Zone,Z3,90;")
123
+ zone.setDouble(1, 45.0)
124
+ print(zone)
125
+ # Zone,
126
+ # Z3, !- Name
127
+ # 45.0; !- Direction of Relative North {deg}
128
+
129
+ # Or built from scratch, with extensible groups
130
+ branch = IdfObject(idd_object=idd.getObject("Branch"))
131
+ branch.setString(0, "Supply Branch")
132
+ branch.push_extensible_group(["Pump:VariableSpeed", "Pump", "Pump-Inlet", "Pump-Outlet"])
133
+
134
+ # The IDD side
135
+ field = idd.getObject("Zone").fields[1]
136
+ print(field.name, field.properties.type, field.properties.units, field.properties.stringDefault)
137
+ # Direction of Relative North real deg 0
138
+ ```
139
+
140
+ Values are validated against the IDD when set (types, choices, bounds), and the IDF is written with its comments
141
+ (`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
142
+
143
+ ## Layout
144
+
145
+ - `energyplus_toolkit/input`: the EnergyPlus input data model (IDD and IDF objects, fields, their types), read from and
146
+ written to files. It doesn't depend on the tools, so it can become its own package.
147
+ - `energyplus_toolkit/format`: the formatter (parallel runner, C++ unit tests: see `format/cpp/README.md`).
148
+ - `energyplus_toolkit/cli.py`: the `energyplus-toolkit` command.
@@ -0,0 +1,116 @@
1
+ # energyplus-toolkit
2
+
3
+ [![pypi](https://img.shields.io/pypi/v/energyplus-toolkit.svg)](https://pypi.org/project/energyplus-toolkit/)
4
+ [![python](https://img.shields.io/pypi/pyversions/energyplus-toolkit.svg)](https://pypi.org/project/energyplus-toolkit/)
5
+ [![Build Status](https://github.com/jmarrec/energyplus-toolkit/actions/workflows/dev.yml/badge.svg)](https://github.com/jmarrec/energyplus-toolkit/actions/workflows/dev.yml)
6
+
7
+ Tools for [EnergyPlus](https://energyplus.net) input files:
8
+
9
+ - parse IDD and IDF files, link each IDF object and field to its IDD definition, and write them back faithfully;
10
+ - reformat IDF and IDD files, and the IDF embedded in EnergyPlus C++ unit tests, from the command line or as
11
+ pre-commit hooks.
12
+
13
+ * GitHub: <https://github.com/jmarrec/energyplus-toolkit>
14
+ * PyPI: <https://pypi.org/project/energyplus-toolkit/>
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install energyplus-toolkit
20
+ ```
21
+
22
+ Requires Python 3.11+.
23
+
24
+ For development:
25
+
26
+ ```bash
27
+ pip install -e ".[dev,test]" # With the dev tools (pre-commit, black, isort, flake8, mypy) and the tests (pytest)
28
+ pre-commit install
29
+ ```
30
+
31
+ ## Command line
32
+
33
+ ```shell
34
+ # Reformat in place, like black: files and/or folders (searched recursively), in parallel
35
+ energyplus-toolkit format idf --idd Energy+.idd testfiles/
36
+ energyplus-toolkit format idd Energy+.idd
37
+ energyplus-toolkit format unit-test --idd Energy+.idd tst/EnergyPlus/unit
38
+
39
+ # Only look: list the files that would change (exit 1 if any), or print a diff
40
+ energyplus-toolkit format idf --idd Energy+.idd --check testfiles/
41
+ energyplus-toolkit format idf --idd Energy+.idd --diff testfiles/1ZoneUncontrolled.idf
42
+ ```
43
+
44
+ Options (see `--help`): `--exclude REGEX`, `-j N` workers, `-q` quiet, `-o OUTPUT` (single file, `-` for stdout),
45
+ and for IDF / unit tests `--leading N` (spaces before object names) and `--keep-integer-reals` (keep `8` instead of
46
+ writing `8.0` in a real field).
47
+
48
+ Exit codes: 0 when done, 1 if `--check` found files to reformat, 123 on error.
49
+
50
+ Content is never lost: a file or block that can't be reformatted safely (unknown object, text outside a field, IMF or
51
+ parametric file, C++ code building the IDF...) is left unchanged, with a warning.
52
+
53
+ ## pre-commit hooks
54
+
55
+ ```yaml
56
+ - repo: https://github.com/jmarrec/energyplus-toolkit
57
+ rev: v0.1.0
58
+ hooks:
59
+ - id: energyplus-format-idd
60
+ args: [--idd, idd/Energy+.idd.in]
61
+ - id: energyplus-format-idf
62
+ args: [--idd, idd/Energy+.idd.in, -q]
63
+ - id: energyplus-format-unit-test
64
+ args: [--idd, idd/Energy+.idd.in, -q]
65
+ ```
66
+
67
+ Each hook needs the IDD to use (`--idd`). For unit tests, put the hook before clang-format: reformatting then
68
+ clang-format is stable.
69
+
70
+ ## Library
71
+
72
+ The IDD gives each IDF object and field its definition: type, units, default, choices, extensible groups...
73
+
74
+ ```python
75
+ from pathlib import Path
76
+
77
+ from energyplus_toolkit.input.idd.idd_file import IddFile
78
+ from energyplus_toolkit.input.idf.idf_file import IdfFile
79
+ from energyplus_toolkit.input.idf.idf_object import IdfObject
80
+
81
+ idd = IddFile.load(Path("Energy+.idd"))
82
+
83
+ # A whole IDF file: each object is linked to its IDD object
84
+ idf = IdfFile.load("in.idf", idd) # or IdfFile.loads(text, idd)
85
+ for zone in idf.get_objects_by_type("Zone"):
86
+ print(zone.name, zone.getDouble(1), zone.getDouble(1, return_default=True)) # "Z2 None 0.0" if left blank
87
+ idf.save("out.idf", overwrite=True) # or str(idf)
88
+
89
+ # A single object, from its text and its IDD object
90
+ zone = IdfObject.load(idd_object=idd.getObject("Zone"), text="Zone,Z3,90;")
91
+ zone.setDouble(1, 45.0)
92
+ print(zone)
93
+ # Zone,
94
+ # Z3, !- Name
95
+ # 45.0; !- Direction of Relative North {deg}
96
+
97
+ # Or built from scratch, with extensible groups
98
+ branch = IdfObject(idd_object=idd.getObject("Branch"))
99
+ branch.setString(0, "Supply Branch")
100
+ branch.push_extensible_group(["Pump:VariableSpeed", "Pump", "Pump-Inlet", "Pump-Outlet"])
101
+
102
+ # The IDD side
103
+ field = idd.getObject("Zone").fields[1]
104
+ print(field.name, field.properties.type, field.properties.units, field.properties.stringDefault)
105
+ # Direction of Relative North real deg 0
106
+ ```
107
+
108
+ Values are validated against the IDD when set (types, choices, bounds), and the IDF is written with its comments
109
+ (`!- Field Name {units}`) and the layout the IDD asks for (`\format`: vertices, compact schedules...).
110
+
111
+ ## Layout
112
+
113
+ - `energyplus_toolkit/input`: the EnergyPlus input data model (IDD and IDF objects, fields, their types), read from and
114
+ written to files. It doesn't depend on the tools, so it can become its own package.
115
+ - `energyplus_toolkit/format`: the formatter (parallel runner, C++ unit tests: see `format/cpp/README.md`).
116
+ - `energyplus_toolkit/cli.py`: the `energyplus-toolkit` command.
@@ -0,0 +1,5 @@
1
+ """Top-level package for energyplus-toolkit."""
2
+
3
+ __author__ = """Julien Marrec"""
4
+ __email__ = 'contact@effibem.com'
5
+ __version__ = '0.1.0'
@@ -0,0 +1,150 @@
1
+ """Command-line interface: energyplus-toolkit COMMAND ...
2
+
3
+ energyplus-toolkit format idf|idd|unit-test PATH...: works like black, give it files and/or folders, they are
4
+ reformatted in place, in parallel. Use --check or --diff to only see what would change.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import re
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from energyplus_toolkit.format import runner
15
+ from energyplus_toolkit.format.runner import Job, Kind, Mode
16
+ from energyplus_toolkit.input.idf import idf_constants
17
+
18
+ DESCRIPTIONS = {
19
+ Kind.IDF: "Reformat EnergyPlus IDF files.",
20
+ Kind.IDD: "Reformat EnergyPlus IDD files.",
21
+ Kind.UNIT_TEST: (
22
+ 'Reformat the IDF embedded in EnergyPlus C++ unit tests, in delimited_string({...}) and R"IDF(...)IDF" blocks.'
23
+ ),
24
+ }
25
+
26
+
27
+ def _add_format_subcommand(subparsers, name: str, kind: Kind) -> None:
28
+ p = subparsers.add_parser(name, help=DESCRIPTIONS[kind], description=DESCRIPTIONS[kind])
29
+ patterns = ", ".join(f"*{s}" for s in kind.suffixes)
30
+ p.add_argument(
31
+ "paths",
32
+ type=Path,
33
+ nargs="+",
34
+ metavar="PATH",
35
+ help=f"Files or folders. Folders are searched recursively for {patterns}.",
36
+ )
37
+ if kind.needs_idd:
38
+ p.add_argument(
39
+ "--idd", dest="idd_path", type=Path, required=True, metavar="IDD_PATH", help="Path to the IDD file."
40
+ )
41
+ mode = p.add_mutually_exclusive_group()
42
+ mode.add_argument(
43
+ "--check", action="store_true", help="Don't write: list the files that would change, exit 1 if any."
44
+ )
45
+ mode.add_argument("--diff", action="store_true", help="Don't write: print a unified diff of the changes.")
46
+ mode.add_argument(
47
+ "-o",
48
+ "--output",
49
+ dest="output_path",
50
+ type=Path,
51
+ metavar="OUTPUT_PATH",
52
+ help="Write to this file instead of in place ('-' for stdout). Only with a single input file.",
53
+ )
54
+ p.add_argument(
55
+ "--exclude",
56
+ type=re.compile,
57
+ metavar="REGEX",
58
+ help="Skip files found in folders whose path matches this regex. Files given explicitly are always processed.",
59
+ )
60
+ p.add_argument(
61
+ "-j",
62
+ "--workers",
63
+ type=int,
64
+ default=None,
65
+ metavar="N",
66
+ help="Number of parallel processes (default: number of CPUs).",
67
+ )
68
+ p.add_argument("-q", "--quiet", action="store_true", help="Only print errors and the summary.")
69
+ if kind is not Kind.IDD:
70
+ p.add_argument(
71
+ "--leading",
72
+ dest="idf_leading",
73
+ type=int,
74
+ default=None,
75
+ metavar="N",
76
+ help=f"Leading spaces before each object-type line (default: {idf_constants.IDF_LEADING}).",
77
+ )
78
+ p.add_argument(
79
+ "--keep-integer-reals",
80
+ action="store_true",
81
+ help='Keep a number written as an integer as is in a real field ("8" instead of "8.0").',
82
+ )
83
+ p.set_defaults(kind=kind)
84
+
85
+
86
+ def get_parser() -> argparse.ArgumentParser:
87
+ parser = argparse.ArgumentParser(prog="energyplus-toolkit", description="Tools for EnergyPlus input files.")
88
+ commands = parser.add_subparsers(dest="command", metavar="COMMAND")
89
+ commands.required = True
90
+
91
+ format_parser = commands.add_parser(
92
+ "format", help="Reformat EnergyPlus files.", description="Reformat EnergyPlus files."
93
+ )
94
+ kinds = format_parser.add_subparsers(dest="kind_name", metavar="KIND")
95
+ kinds.required = True
96
+ _add_format_subcommand(kinds, "idf", Kind.IDF)
97
+ _add_format_subcommand(kinds, "idd", Kind.IDD)
98
+ _add_format_subcommand(kinds, "unit-test", Kind.UNIT_TEST)
99
+ return parser
100
+
101
+
102
+ def _make_job(args, parser: argparse.ArgumentParser) -> Job:
103
+ kind: Kind = args.kind
104
+ idd_path = getattr(args, "idd_path", None)
105
+ if idd_path is not None and not idd_path.is_file():
106
+ parser.error(f"IDD file not found: {idd_path}")
107
+ constants: list[tuple[str, object]] = []
108
+ if getattr(args, "idf_leading", None) is not None:
109
+ if args.idf_leading < 0:
110
+ parser.error("--leading must be a non-negative integer.")
111
+ constants.append(("IDF_LEADING", args.idf_leading))
112
+ if getattr(args, "keep_integer_reals", False):
113
+ constants.append(("IDF_KEEP_INTEGER_REALS", True))
114
+ mode = Mode.CHECK if args.check else Mode.DIFF if args.diff else Mode.WRITE
115
+ return Job(kind=kind, mode=mode, idd_path=idd_path, constants=tuple(constants))
116
+
117
+
118
+ def main(args_: list[str] | None = None) -> int:
119
+ parser = get_parser()
120
+ args = parser.parse_args(args_)
121
+
122
+ for path in args.paths:
123
+ if not path.exists():
124
+ parser.error(f"path not found: {path}")
125
+ if args.workers is not None and args.workers < 1:
126
+ parser.error("--workers must be at least 1.")
127
+ job = _make_job(args, parser)
128
+
129
+ if args.output_path is not None:
130
+ # Single file, written elsewhere
131
+ if len(args.paths) != 1 or args.paths[0].is_dir():
132
+ parser.error("-o/--output needs exactly one input file.")
133
+ runner.init_worker(job)
134
+ text = runner.reformat_text(job.kind, args.paths[0].read_text(encoding="utf-8"))
135
+ if str(args.output_path) == "-":
136
+ sys.stdout.write(text)
137
+ else:
138
+ args.output_path.write_text(text, encoding="utf-8")
139
+ return 0
140
+
141
+ files = runner.collect_files(args.paths, job.kind, args.exclude)
142
+ if not files:
143
+ print("No files to reformat.", file=sys.stderr)
144
+ return 0
145
+ results = runner.run(files, job, workers=args.workers)
146
+ return runner.report(results, job, quiet=args.quiet)
147
+
148
+
149
+ if __name__ == "__main__": # pragma: no cover
150
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Formatting EnergyPlus files: IDF, IDD, and the IDF in EnergyPlus C++ unit tests."""