Skip to content

Resolve referenced .sw files for a configuration#21

Open
sclaiborne wants to merge 3 commits into
mainfrom
feature/config-sw-reference-resolution
Open

Resolve referenced .sw files for a configuration#21
sclaiborne wants to merge 3 commits into
mainfrom
feature/config-sw-reference-resolution

Conversation

@sclaiborne

Copy link
Copy Markdown
Member

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 .sw but references another configuration's (several configs sharing one software deployment). Previously, code that only looked for a physically-present Cpu.sw would miss those configurations entirely.

What's added

paths

  • resolveReferencePath(refText, baseDir, projectRoot) — resolve a package Reference="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. taskSources skips tasks with an empty Source (e.g. binary-only HMI tasks).

config (CpuConfig)

  • objectList, plus getSoftwareConfigPath() / getSwDeploymentTable() which resolve a local or referenced .sw (auto-detecting the project root when not supplied).

project (Project)

  • getConfigCpus(), getConfigSwTables(), getConfigTaskSources() — walk Config.pkg → Cpu.pkg → .sw for 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 .sw files under each config silently returns nothing for B/C/D. Resolving the reference makes getConfigTaskSources() return the real task list for every configuration.

Tests

7 new cases in tests/test_paths.py covering resolveReferencePath (all four reference shapes + empty/None) and findProjectRoot (found via a temp .apj, and None when absent). All 16 test_paths.py cases pass.

🤖 Generated with Claude Code

sclaiborne and others added 2 commits June 8, 2026 14:59
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() and paths.findProjectRoot() to resolve referenced package targets and detect project roots.
  • Extended CpuConfig, SwDeploymentTable, and Project with APIs to traverse Config.pkg → Cpu.pkg → .sw and 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 thread aspython/paths.py Outdated
Comment thread aspython/config.py
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 thread aspython/deployment.py
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 thread aspython/project.py
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>
@sclaiborne

Copy link
Copy Markdown
Member Author

Thanks for the review @copilot-pull-request-reviewer — agreed with all four points; addressed in 8c01446:

  1. resolveReferencePath None type hint — typed refText as Optional[str] to match the documented/​tested behavior.
  2. Referenced .sw coverage — added tests/test_config_sw.py, hermetic tests for CpuConfig.getSoftwareConfigPath: local file, Reference="true" with ..-relative and project-root-relative targets, project-root auto-detection via .apj, and the missing-file → None case. These need no lpm fixture, so they run on every Python version.
  3. taskElements/taskNames/taskSources — added assertions in tests/test_deployment.py (non-empty tasks, names align with elements, all sources truthy, and source count bounded by task count to lock in the empty-Source omission).
  4. Project traversal APIs — added tests/test_config_traversal.py exercising getConfigCpus/getConfigSwTables/getConfigTaskSources against the Intel config end-to-end (plus an unknown-config case), registered as AsProject-dependent so it skips without lpm and runs in CI.

All jobs (3.11–3.14) pass, including the new fixture-backed tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants