Refactor into aspython package with unified CLI#17
Conversation
Split the monolithic ASTools.py into a proper aspython package with focused submodules (project, library, package, task, deployment, config, build, simulation, paths, models, xml_base, returncodes, utils, installer, hmi, unittests, upgrades, cnc, logging_setup) and consolidate the nine CmdLine*.py scripts behind a single aspython CLI with subcommands. Highlights: - New aspython package + aspython CLI (build, arsim, export-libs, deploy-libs, safety-crc, version, installer, package-hmi, run-tests). - Backwards compatible: import ASTools and python CmdLineXxx.py ... still work via deprecation shims. - Added pyproject.toml (editable install + console_scripts + dev extras), PyInstaller spec, and GitHub Actions CI. - Added pytest suite (57 tests) covering imports, models, path helpers, and CLI dispatch. - Bumped version to 0.3.0; updated CHANGELOG and README migration table.
Resolves the conflict introduced by the AS6 fix on main (#16). The fix to ASTools.getASPath / Project._parseASVersion has been ported into the new package locations: - aspython/paths.py: added _AS_BASE_CANDIDATES + _findASBase() so AS6 (which installs under 'C:\Program Files (x86)\BRAutomation') is discovered alongside the legacy 'C:\BrAutomation'. - aspython/project.py: Project._parseASVersion now returns 'AS6' for AS 6.x (major-only folder) and keeps 'AS<major><minor>' for AS <= 4.x. - tests/test_paths.py: covers _findASBase preference + legacy fallback. - tests/test_project.py: covers AS4/AS6 version parsing. The top-level ASTools.py shim is kept unchanged (kept ours).
- Remove 'aspython.installer_assets' from packages (directory was never created; the .iss/.bat assets live in top-level Files/ instead). - Switch project.license from deprecated TOML table form to SPDX string 'MIT'.
There was a problem hiding this comment.
Pull request overview
Refactors the legacy monolithic Automation Studio helper scripts into a structured aspython Python package with a unified aspython CLI, while keeping existing ASTools.py / CmdLine*.py entry points working via deprecation shims.
Changes:
- Introduces the
aspythonpackage with split-out modules for project/library/package/build/CLI functionality. - Adds a single unified CLI (
python -m aspython/aspython) with subcommands replacing the legacyCmdLine*.pyscripts. - Adds packaging + CI tooling (
pyproject.toml, GitHub Actions) and a new pytest suite.
Reviewed changes
Copilot reviewed 59 out of 59 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/ci.yml |
Adds Windows CI running ruff + pytest and release-tag exe artifact steps. |
ASCncConfig.py |
Converts legacy CNC helpers into a deprecation shim re-exporting aspython.cnc. |
ASTools.py |
Converts the legacy monolith into a deprecation shim re-exporting aspython API. |
CHANGELOG.md |
Documents the 0.3.0 refactor and migration notes. |
CmdLineARSim.py |
Deprecation shim delegating to aspython arsim. |
CmdLineBuild.py |
Deprecation shim delegating to aspython build. |
CmdLineCreateInstaller.py |
Deprecation shim delegating to aspython installer. |
CmdLineDeployLibraries.py |
Deprecation shim delegating to aspython deploy-libs. |
CmdLineExportLib.py |
Deprecation shim delegating to aspython export-libs. |
CmdLineGetSafetyCrc.py |
Deprecation shim delegating to aspython safety-crc. |
CmdLineGetVersion.py |
Deprecation shim delegating to aspython version. |
CmdLinePackageHmi.py |
Deprecation shim delegating to aspython package-hmi. |
CmdLineRunUnitTests.py |
Deprecation shim delegating to aspython run-tests. |
ColorCodedLog.py |
Keeps legacy colored logging API as a deprecation shim and points to aspython.logging_setup. |
ExportLibraries.py |
Deprecates/removes the parameter-file driven export path in favor of the new CLI. |
InstallUpgrades.py |
Updates standalone upgrade installer CLI to use aspython.upgrades + shared logging setup. |
README.md |
Adds 0.3.0 migration table and unified CLI guidance. |
UnitTestTools.py |
Deprecation shim re-exporting aspython.unittests.UnitTestServer. |
_version.py |
Back-compat shim to re-export aspython._version.__version__. |
aspython/__init__.py |
Defines the public re-export surface for the new package. |
aspython/__main__.py |
Enables python -m aspython to invoke the unified CLI. |
aspython/_version.py |
Sets package version to 0.3.0. |
aspython/build.py |
Implements build orchestration via BR.AS.Build.exe. |
aspython/cli/__init__.py |
Declares the unified CLI package. |
aspython/cli/arsim.py |
Implements aspython arsim subcommand. |
aspython/cli/build.py |
Implements aspython build subcommand. |
aspython/cli/deploy_libs.py |
Implements aspython deploy-libs subcommand. |
aspython/cli/export_libs.py |
Implements aspython export-libs subcommand. |
aspython/cli/installer.py |
Implements aspython installer subcommand. |
aspython/cli/main.py |
Root CLI wiring, shared flags, and subcommand registration. |
aspython/cli/package_hmi.py |
Implements aspython package-hmi subcommand. |
aspython/cli/run_tests.py |
Implements aspython run-tests subcommand. |
aspython/cli/safety_crc.py |
Implements aspython safety-crc subcommand. |
aspython/cli/version.py |
Implements aspython version subcommand. |
aspython/cnc.py |
CNC helpers with an import guard intended for optional lxml. |
aspython/config.py |
Adds CpuConfig class split from the legacy monolith. |
aspython/deployment.py |
Adds SwDeploymentTable handling split from legacy code. |
aspython/hmi.py |
Adds HMI packaging helpers split from legacy scripts. |
aspython/installer.py |
Adds Inno Setup compilation helpers split from legacy scripts. |
aspython/library.py |
Adds Library class split from legacy code. |
aspython/logging_setup.py |
Centralizes logging setup + Windows ANSI console enablement. |
aspython/models.py |
Adds dataclass value objects (BuildConfig, export info, dependencies). |
aspython/package.py |
Adds Package class split from legacy code. |
aspython/paths.py |
Adds AS path helpers and conversions split from legacy code. |
aspython/project.py |
Adds Project class split from legacy code. |
aspython/returncodes.py |
Moves return code tables into a dedicated module. |
aspython/simulation.py |
Adds ARsim structure creation helper split from legacy code. |
aspython/task.py |
Adds Task class split from legacy code. |
aspython/unittests.py |
Adds UnitTestServer HTTP client split from legacy code. |
aspython/upgrades.py |
Adds upgrade install helper split from legacy code. |
aspython/utils.py |
Moves toDict helper into a dedicated module. |
aspython/xml_base.py |
Adds xmlAsFile base class for reading/writing AS XML. |
pyproject.toml |
Adds setuptools-based packaging config, console script entrypoint, and dev extras. |
tests/conftest.py |
Ensures repo-root imports work under pytest without editable install. |
tests/test_cli_smoke.py |
CLI smoke tests for root and each subcommand help/version behavior. |
tests/test_models.py |
Tests for the new dataclass model types and back-compat constructor behavior. |
tests/test_paths.py |
Tests for path helper utilities. |
tests/test_smoke_imports.py |
Smoke-import test and public API surface check for aspython. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def buildASProject( | ||
| project, | ||
| ASPath: str, | ||
| configuration: str = '', | ||
| buildMode: str = 'Build', | ||
| buildRUCPackage: bool = True, | ||
| tempPath: str = '', | ||
| binaryPath: str = '', | ||
| logPath: str = '', | ||
| simulation: bool = False, | ||
| additionalArg: Union[str, list, tuple, None] = None, | ||
| ) -> subprocess.CompletedProcess: | ||
| commandLine = [ASPath, '"' + os.path.abspath(project) + '"'] | ||
|
|
||
| if configuration: | ||
| commandLine.extend(['-c', configuration]) | ||
|
|
||
| if buildMode: | ||
| commandLine.extend(['-buildMode', buildMode]) | ||
| if buildMode.capitalize() == 'Rebuild': | ||
| commandLine.append('-all') | ||
|
|
||
| if tempPath: | ||
| commandLine.extend(['-t', tempPath]) | ||
|
|
||
| if binaryPath: | ||
| commandLine.extend(['-o', binaryPath]) | ||
|
|
||
| if simulation: | ||
| commandLine.append('-simulation') | ||
|
|
||
| if buildRUCPackage: | ||
| commandLine.append('-buildRUCPackage') | ||
|
|
||
| if additionalArg: | ||
| if isinstance(additionalArg, str): | ||
| commandLine.append(additionalArg) | ||
| elif isinstance(additionalArg, (list, tuple)): | ||
| commandLine.extend(additionalArg) | ||
|
|
||
| logging.info(f'Starting build for configuration {configuration}...') | ||
| logging.debug(commandLine) | ||
| process = subprocess.Popen(commandLine, stdout=subprocess.PIPE, encoding="utf-8", errors='replace') | ||
|
|
||
| log_file = os.path.join(logPath, "build.log") | ||
| logging.info("Recording build log here: " + log_file) | ||
|
|
||
| with open(log_file, "w", encoding='utf-8') as f: | ||
| while process.returncode is None: | ||
| raw = process.stdout.readline() | ||
| data = raw.rstrip() | ||
| f.write(raw) | ||
| if data != "": | ||
| warningMatch = re.search('warning [0-9]*:', data) | ||
| errorMatch = re.search('error [0-9]*:', data) | ||
| if warningMatch is not None: | ||
| logging.warning("\033[32m" + data + "\033[0m") | ||
| elif errorMatch is not None: | ||
| logging.error("\033[31m" + data + "\033[0m") | ||
| else: | ||
| logging.debug(data) | ||
| process.poll() | ||
|
|
||
| return process |
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Fixed in commit a59725c — buildASProject now returns subprocess.CompletedProcess(commandLine, process.returncode) instead of the raw Popen instance. The streaming/logging behavior is fully preserved (output is written line-by-line to the log file and via logging); callers only access .returncode so no captured stdout is needed on the returned object.
| def add_subparser(subparsers): | ||
| p = subparsers.add_parser(SUBCOMMAND, help=HELP, description=HELP) | ||
| p.add_argument('host', type=str, help='IP address of the PLC running the tests') | ||
| p.add_argument('-d', '--destination', type=str, required=True, | ||
| help='Destination directory for the test result XML files') | ||
| p.add_argument('-a', '--all', action='store_true', | ||
| help='Run all available tests') | ||
| p.set_defaults(func=run) | ||
| return p | ||
|
|
||
|
|
||
| def run(args) -> int: | ||
| logging.debug('args: %s', args) | ||
| logging.info('Querying test server to retrieve list of available tests') | ||
|
|
||
| testServer = UnitTestServer(args.host, args.destination) | ||
| if not testServer.connected: | ||
| logging.error('Could not connect to the test server') | ||
| return 1 | ||
|
|
||
| for testSuite in testServer.testSuites: | ||
| logging.info(f'Running test suite {testSuite["device"]}') | ||
| testServer.runTest(testSuite['device']) |
| dependencies = [ | ||
| "requests", | ||
| "lxml", | ||
| ] |
Bugs fixed (clearly correct, mostly pre-existing in legacy ASTools.py): - library.py addObject: missing () on os.path.isdir guard; copy into self.dirPath instead of self.path (.lby file). - library.py addDependency: replace 'is not Dependency' (always true) with isinstance check; append the XML element to the Dependencies container instead of the cached _dependencies list. - project.py exportLibraries: actually pass the buildConfigs parameter to lib.export instead of always self.buildConfigs. - project.py createArsim: splat *configNames and forward destination so the legacy alias is callable. - paths.py getAsPathType: guard against empty string so it returns None instead of IndexError. - cli/export_libs.py: --configuration is now required; --whitelist/--blacklist default to [] (was ''). - cli/installer.py: --output and --appName are now required so the iscc command line never contains literal 'None'. - pyproject.toml: lxml moved out of required dependencies into a 'cnc' extra (matches the optional import guard in aspython.cnc); kept under [dev] so tests still cover it.
Review responsePushed Fixed in this push (clearly correct, mostly pre-existing legacy bugs)
All 62 tests still pass. Flagged for your review (intentional or pre-existing behavior — did not change)These are real points but the changes are either behavior-preserving risks I don't want to slip into this refactor PR, or design decisions worth a separate ticket. Tagging @scott for sign-off:
Happy to address any of the flagged items in a follow-up PR — let me know which ones you want done now vs. deferred. |
Agent-Logs-Url: https://github.com/loupeteam/ASPython/sessions/e507d609-fa6a-4970-b174-dbdf94613c78 Co-authored-by: sclaiborne <29549528+sclaiborne@users.noreply.github.com>
Agent-Logs-Url: https://github.com/loupeteam/ASPython/sessions/1f18a51e-fdf1-46f2-8cc3-394a853bad86 Co-authored-by: sclaiborne <29549528+sclaiborne@users.noreply.github.com>
…annotation Agent-Logs-Url: https://github.com/loupeteam/ASPython/sessions/8a60689c-ba00-4464-97c4-63b5584e8b12 Co-authored-by: sclaiborne <29549528+sclaiborne@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Summary
Major maintainability refactor that breaks the monolithic
ASTools.py(and the nine standaloneCmdLine*.pyscripts) into a properaspythonpackage with a single unified CLI, while keeping every legacy entry point working.What changed
New
aspythonpackageASTools.pywas split into 21 focused modules:aspython.projectProjectclassaspython.libraryLibraryclassaspython.packagePackageclassaspython.taskTaskclassaspython.deploymentSwDeploymentTableaspython.configCpuConfigaspython.buildbuildASProject/batchBuildAsProject/ASProjetGetConfigsaspython.simulationCreateARSimStructureaspython.pathsgetASPath,convertAsPathToWinPath, ...)aspython.modelsBuildConfig,Dependency,LibExportInfo,ProjectExportInfo)aspython.xml_basexmlAsFilebase classaspython.returncodesASReturnCodes,PVIReturnCodeTextaspython.utilstoDictaspython.installeraspython.hmiaspython.unittestsUnitTestServeraspython.upgradesinstallBRUpgradeaspython.cnclxmlimport)aspython.logging_setupUnified
aspythonCLIAll nine
CmdLine*.pyscripts are now subcommands of a singleaspythonconsole script:CmdLineBuild.pyaspython buildCmdLineARSim.pyaspython arsimCmdLineExportLib.pyaspython export-libsCmdLineDeployLibraries.pyaspython deploy-libsCmdLineGetSafetyCrc.pyaspython safety-crcCmdLineGetVersion.pyaspython versionCmdLineCreateInstaller.pyaspython installerCmdLinePackageHmi.pyaspython package-hmiCmdLineRunUnitTests.pyaspython run-testsEach subcommand lives in its own
aspython/cli/<name>.pymodule exposingadd_subparser/run, andaspython/cli/main.pywires them up with shared-l/--logLeveland-v/--versionflags.Backwards compatibility
Nothing existing breaks:
import ASToolsstill resolves and re-exports the full public API, with aDeprecationWarning.ColorCodedLog,UnitTestTools,ASCncConfig, and top-level_versionare also re-export shims.CmdLine*.pyscript is now a tiny shim that delegates toaspython <sub>so existing CI / batch files keep working (and emit aDeprecationWarningpointing at the new command).InstallUpgrades.pykeeps its standalone CLI but importsinstallBRUpgradefromaspython.upgrades.Packaging & tooling
pyproject.tomlwith setuptools backend, dynamic version fromaspython._version,aspythonconsole_scripts entry, and a[dev]extra (pytest,ruff,pyinstaller).packaging/aspython.specto build a single-fileaspython.exevia PyInstaller..github/workflows/ci.ymlruns pytest + ruff on Windows across Python 3.10 / 3.11 / 3.12 and attaches the built exe to tagged releases.Tests
New
tests/suite (57 tests, all green):test_smoke_imports.py— every submodule imports cleanly + the public API surface is preserved.test_models.py—BuildConfig(incl. legacytyp=kwarg),Dependency,ProjectExportInfopartition / extend.test_paths.py— pure path helpers.test_cli_smoke.py—python -m aspython --help+--helpfor every subcommand +import ASToolsregression check.Version
Bumped to 0.3.0 with a CHANGELOG entry and a migration table at the top of the README.
Migration
Existing users:
Old invocations (
python CmdLineBuild.py ...,import ASTools) keep working but print aDeprecationWarning.Test plan
python -m pytest tests -q→ 57 passedpython -m aspython --helplists all 9 subcommandspython -m aspython <sub> --helpworks for every subcommandimport ASToolsstill exposesProject/Library/buildASProject/ASReturnCodes