Resolve referenced .sw files for a configuration#21
Open
sclaiborne wants to merge 3 commits into
Open
Conversation
Add a reusable way to get all software-deployment (.sw) information for a configuration, including the common case where a configuration does not own its .sw but references another configuration's (e.g. several configs sharing one software deployment). paths: - resolveReferencePath(): resolve a package Reference="true" target (absolute, project-root-relative with/without leading slash, or ..-relative) to a normalized Windows path. - findProjectRoot(): walk up to the directory containing the .apj. deployment (SwDeploymentTable): - taskElements / taskNames / taskSources across all task classes (taskSources skips tasks with an empty Source). config (CpuConfig): - objectList, plus getSoftwareConfigPath()/getSwDeploymentTable() which resolve a local or referenced .sw (auto-detecting the project root). project (Project): - getConfigCpus(), getConfigSwTables(), getConfigTaskSources() to walk Config.pkg -> Cpu.pkg -> .sw for every CPU in a configuration, resolving references transparently. Tests: 7 cases for resolveReferencePath / findProjectRoot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ntpath.isabs treated a rooted-but-driveless path (\Logical\...) as absolute before Python 3.13, so resolveReferencePath returned such project-root-relative references unchanged on 3.11/3.12 (CI failure). Detect a true absolute path via the drive/UNC prefix instead of isabs, which is stable across versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end helpers for discovering software-deployment (.sw) data for a given Automation Studio configuration, including the important case where a configuration references another configuration’s .sw via Reference="true" rather than owning a local Cpu.sw.
Changes:
- Added
paths.resolveReferencePath()andpaths.findProjectRoot()to resolve referenced package targets and detect project roots. - Extended
CpuConfig,SwDeploymentTable, andProjectwith APIs to traverseConfig.pkg → Cpu.pkg → .swand extract deployed task sources. - Added unit tests for the new path helpers.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_paths.py |
Adds coverage for resolveReferencePath() and findProjectRoot(). |
aspython/project.py |
Introduces configuration-level traversal helpers to collect CPUs, .sw tables, and task sources. |
aspython/paths.py |
Adds reference resolution + project-root discovery utilities. |
aspython/deployment.py |
Adds task enumeration helpers (taskElements, taskNames, taskSources). |
aspython/config.py |
Adds .sw resolution on CpuConfig, including reference handling and root auto-detection. |
aspython/__init__.py |
Exposes new path helpers at the package top level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+48
to
+75
| def getSoftwareConfigPath(self, projectRoot: Optional[str] = None) -> Optional[str]: | ||
| """Resolve this CPU's software-configuration (``.sw``) file on disk. | ||
|
|
||
| The ``.sw`` may be a file owned by this CPU folder, or a reference to | ||
| another configuration's ``.sw`` (common when several configurations | ||
| share one software deployment). References are resolved relative to the | ||
| project root, which is auto-detected from the package location when | ||
| *projectRoot* is not given. | ||
| """ | ||
| if projectRoot is None: | ||
| projectRoot = findProjectRoot(self.path) or self.dirPath | ||
| for obj in self.objectList: | ||
| text = (obj.text or '').strip() | ||
| if not text.lower().endswith('.sw'): | ||
| continue | ||
| if obj.attrib.get('Reference', 'false').lower() == 'true': | ||
| return resolveReferencePath(text, self.dirPath, projectRoot) | ||
| return os.path.normpath(os.path.join(self.dirPath, text)) | ||
| # Fall back to the conventional local file name. | ||
| local = os.path.join(self.dirPath, 'Cpu.sw') | ||
| return os.path.normpath(local) if os.path.isfile(local) else None | ||
|
|
||
| def getSwDeploymentTable(self, projectRoot: Optional[str] = None) -> Optional[SwDeploymentTable]: | ||
| """Return the resolved :class:`SwDeploymentTable`, or ``None`` if absent.""" | ||
| swPath = self.getSoftwareConfigPath(projectRoot) | ||
| if swPath and os.path.isfile(swPath): | ||
| return SwDeploymentTable(swPath) | ||
| return None |
Comment on lines
+110
to
+128
| @property | ||
| def taskElements(self) -> List[ET.Element]: | ||
| """Every ``<Task>`` deployed in this table, across all task classes.""" | ||
| tasks: List[ET.Element] = [] | ||
| for tc in self.findall('TaskClass'): | ||
| tasks.extend(tc.findall(self.nameSpaceFormatted + 'Task')) | ||
| return tasks | ||
|
|
||
| @property | ||
| def taskNames(self) -> List[str]: | ||
| return [t.get('Name', '') for t in self.taskElements] | ||
|
|
||
| @property | ||
| def taskSources(self) -> List[str]: | ||
| """The ``Source`` of every deployed task (e.g. ``Pkg.Sub.Name.prg``). | ||
|
|
||
| Tasks with an empty ``Source`` (e.g. binary-only HMI tasks) are omitted. | ||
| """ | ||
| return [src for t in self.taskElements if (src := t.get('Source'))] |
Comment on lines
+218
to
+257
| def getConfigCpus(self, configName: str) -> List[CpuConfig]: | ||
| """Return a :class:`CpuConfig` for each CPU deployed in *configName*. | ||
|
|
||
| The CPUs are read from the configuration's ``Config.pkg`` (objects of | ||
| ``Type="Cpu"``), so this works regardless of the hardware folder name | ||
| and supports configurations that contain more than one CPU. | ||
| """ | ||
| configDir = os.path.join(self.physicalPath, configName) | ||
| configPkg = os.path.join(configDir, 'Config.pkg') | ||
| cpus: List[CpuConfig] = [] | ||
| if not os.path.isfile(configPkg): | ||
| return cpus | ||
| for obj in Package(configPkg).objectList: | ||
| if obj.get('Type', '').lower() != 'cpu': | ||
| continue | ||
| cpuPkg = os.path.join(configDir, (obj.text or '').strip(), 'Cpu.pkg') | ||
| if os.path.isfile(cpuPkg): | ||
| cpus.append(CpuConfig(cpuPkg)) | ||
| return cpus | ||
|
|
||
| def getConfigSwTables(self, configName: str) -> List[SwDeploymentTable]: | ||
| """Return every software-deployment table (``.sw``) used by *configName*. | ||
|
|
||
| One table is returned per CPU. References to another configuration's | ||
| ``.sw`` are resolved transparently, so a configuration that merely | ||
| points at a shared deployment still yields its real task/library list. | ||
| """ | ||
| tables: List[SwDeploymentTable] = [] | ||
| for cpu in self.getConfigCpus(configName): | ||
| table = cpu.getSwDeploymentTable(self.dirPath) | ||
| if table is not None: | ||
| tables.append(table) | ||
| return tables | ||
|
|
||
| def getConfigTaskSources(self, configName: str) -> List[str]: | ||
| """Return the ``Source`` of every task deployed by *configName*'s CPUs.""" | ||
| sources: List[str] = [] | ||
| for table in self.getConfigSwTables(configName): | ||
| sources.extend(table.taskSources) | ||
| return sources |
- resolveReferencePath: type refText as Optional[str] (it already accepts
None and is called with None), so the hint matches behavior.
- Add hermetic tests (test_config_sw.py) for CpuConfig.getSoftwareConfigPath:
local .sw, Reference="true" with ..-relative and project-root-relative
targets, project-root auto-detection via .apj, and the missing-file ->
None case. These run on every Python version (no lpm fixture needed).
- Add fixture-backed coverage for the new parsing/traversal APIs:
- test_deployment.py: taskElements/taskNames/taskSources (incl. empty
Source omission).
- test_cpu_config.py: getSoftwareConfigPath / getSwDeploymentTable.
- test_config_traversal.py: end-to-end getConfigCpus/getConfigSwTables/
getConfigTaskSources against the AsProject 'Intel' config, registered
as AsProject-dependent so it skips without lpm and runs in CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Member
Author
|
Thanks for the review @copilot-pull-request-reviewer — agreed with all four points; addressed in 8c01446:
All jobs (3.11–3.14) pass, including the new fixture-backed tests. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a reusable way to get all software-deployment (
.sw) information for a configuration — including the common case where a configuration does not own its.swbut references another configuration's (several configs sharing one software deployment). Previously, code that only looked for a physically-presentCpu.swwould miss those configurations entirely.What's added
pathsresolveReferencePath(refText, baseDir, projectRoot)— resolve a packageReference="true"target to a normalized Windows path. Handles the shapes AS uses: absolute, project-root-relative (with or without a leading\), and..-relative.findProjectRoot(startPath)— walk up to the directory containing the.apj.deployment(SwDeploymentTable)taskElements/taskNames/taskSources— every<Task>across all task classes.taskSourcesskips tasks with an emptySource(e.g. binary-only HMI tasks).config(CpuConfig)objectList, plusgetSoftwareConfigPath()/getSwDeploymentTable()which resolve a local or referenced.sw(auto-detecting the project root when not supplied).project(Project)getConfigCpus(),getConfigSwTables(),getConfigTaskSources()— walkConfig.pkg → Cpu.pkg → .swfor every CPU in a configuration, resolving references transparently. Works regardless of the hardware-folder name and supports multi-CPU configurations.Why
A configuration's deployed tasks are the entry point for tooling that maps source files to the configurations that ship them. When configs B/C/D reference config A's
Cpu.sw, a glob for.swfiles under each config silently returns nothing for B/C/D. Resolving the reference makesgetConfigTaskSources()return the real task list for every configuration.Tests
7 new cases in
tests/test_paths.pycoveringresolveReferencePath(all four reference shapes + empty/None) andfindProjectRoot(found via a temp.apj, andNonewhen absent). All 16test_paths.pycases pass.🤖 Generated with Claude Code