jcl-parser 2.3.0__tar.gz → 2.3.1__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 (80) hide show
  1. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/PKG-INFO +1 -1
  2. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/PKG-INFO +1 -1
  3. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/__init__.py +1 -1
  4. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_job_builder.py +24 -4
  5. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_statement_parser.py +1 -1
  6. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/explosion.py +3 -2
  7. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/include_loader.py +39 -7
  8. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lineage.py +38 -2
  9. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/pyproject.toml +1 -1
  10. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/LICENSE +0 -0
  11. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/README.md +0 -0
  12. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/SOURCES.txt +0 -0
  13. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/dependency_links.txt +0 -0
  14. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/entry_points.txt +0 -0
  15. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/requires.txt +0 -0
  16. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jcl_parser.egg-info/top_level.txt +0 -0
  17. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/__main__.py +0 -0
  18. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_include_origins.py +0 -0
  19. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_model_factories.py +0 -0
  20. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_post_parse.py +0 -0
  21. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/_source_scanner.py +0 -0
  22. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/builtin_procs.py +0 -0
  23. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/checks.py +0 -0
  24. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/cli.py +0 -0
  25. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/condition_parser.py +0 -0
  26. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/continuation_handler.py +0 -0
  27. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/dataset_lifecycle.py +0 -0
  28. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/errors.py +0 -0
  29. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/jcl_file_type.py +0 -0
  30. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/jes_statements.py +0 -0
  31. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/__init__.py +0 -0
  32. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/base.py +0 -0
  33. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/config.py +0 -0
  34. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/rules.py +0 -0
  35. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/runner.py +0 -0
  36. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/lint/suppress.py +0 -0
  37. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/metrics.py +0 -0
  38. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/model.py +0 -0
  39. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/parameter_ast.py +0 -0
  40. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/parameter_validators.py +0 -0
  41. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/parser.py +0 -0
  42. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/plugins/__init__.py +0 -0
  43. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/plugins/base.py +0 -0
  44. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/proc_loader.py +0 -0
  45. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/profiling.py +0 -0
  46. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/__init__.py +0 -0
  47. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/_control.py +0 -0
  48. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/_invoked.py +0 -0
  49. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/adrdssu.py +0 -0
  50. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/base.py +0 -0
  51. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/binder.py +0 -0
  52. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/bpxbatch.py +0 -0
  53. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/compilers.py +0 -0
  54. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/db2_batch.py +0 -0
  55. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/dfsrrc00.py +0 -0
  56. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/dsnutilb.py +0 -0
  57. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/ftp.py +0 -0
  58. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/icetool.py +0 -0
  59. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/idcams.py +0 -0
  60. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iebcompr.py +0 -0
  61. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iebdg.py +0 -0
  62. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iebgener.py +0 -0
  63. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iebptpch.py +0 -0
  64. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iebupdte.py +0 -0
  65. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iefbr14.py +0 -0
  66. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iehinitt.py +0 -0
  67. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iehlist.py +0 -0
  68. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/iehprogm.py +0 -0
  69. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/ikjeft01.py +0 -0
  70. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/irxjcl.py +0 -0
  71. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/registry.py +0 -0
  72. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/program_profiles/sort.py +0 -0
  73. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/py.typed +0 -0
  74. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/rc_analysis.py +0 -0
  75. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/serialization.py +0 -0
  76. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/sql_detector.py +0 -0
  77. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/system_symbols.py +0 -0
  78. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/utils.py +0 -0
  79. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/jclparser/validators.py +0 -0
  80. {jcl_parser-2.3.0 → jcl_parser-2.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jcl-parser
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
5
5
  Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jcl-parser
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
5
5
  Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -20,7 +20,7 @@ application, for example::
20
20
  import logging as _logging
21
21
 
22
22
  # Package metadata
23
- __version__ = "2.3.0" # x-release-please-version
23
+ __version__ = "2.3.1" # x-release-please-version
24
24
  __author__ = "JCL Parser Team"
25
25
  __description__ = "Parse IBM JCL scripts into Python objects"
26
26
 
@@ -621,11 +621,31 @@ class JobBuilder:
621
621
  sink.append(member)
622
622
  # Found members are expanded before the parse loop runs, so an
623
623
  # INCLUDE that reaches here could not be loaded.
624
- self._warnings_sink().append(
625
- f"Line {stmt.line_number}: INCLUDE member {member} not found in "
626
- "the JCLLIB / procedure libraries; its statements are missing "
627
- "from the job"
624
+ self._warnings_sink().append(self._include_warning(stmt, str(member)))
625
+
626
+ def _include_warning(self, stmt: Statement, member: str) -> str:
627
+ """The warning for an INCLUDE whose member could not be loaded."""
628
+ line = f"Line {stmt.line_number}: INCLUDE member"
629
+ missing = " its statements are missing from the job"
630
+ if "&" in member:
631
+ from .include_loader import _substitute_member_symbols
632
+
633
+ symbols = (
634
+ {**self.pending_sets, **self.job.set_statements}
635
+ if self.job
636
+ else self.pending_sets
628
637
  )
638
+ resolved = _substitute_member_symbols(member, symbols)
639
+ if "&" in resolved:
640
+ # Not a missing member: its name could not be built (#180).
641
+ return (
642
+ f"{line} {member} uses a symbol with no SET value before "
643
+ f"it;{missing}"
644
+ )
645
+ member = f"{resolved} (from {member})"
646
+ return (
647
+ f"{line} {member} not found in the JCLLIB / procedure libraries;{missing}"
648
+ )
629
649
 
630
650
  def _on_conditional(self, stmt: Statement, line_num: int) -> None:
631
651
  # Conditional logic - track conditionals properly
@@ -803,7 +803,7 @@ def _extract_jcllib_order(content: str) -> list[str]:
803
803
  symbols: dict[str, str] = {}
804
804
  lines = split_source_lines(content)
805
805
  for index, line in enumerate(lines):
806
- _record_set_symbols(line.lstrip(), symbols)
806
+ _record_set_symbols(line.lstrip(), symbols, lines, index + 1)
807
807
  params_part = _jcllib_operands(line)
808
808
  if params_part is None:
809
809
  continue
@@ -1898,8 +1898,9 @@ class _IncludeExpandingLoader(ProcedureLoader):
1898
1898
  from .utils import split_source_lines
1899
1899
 
1900
1900
  seen = dict(symbols) if symbols is not None else {}
1901
- for line in split_source_lines(expanded_text):
1902
- _record_set_symbols(line.rstrip(), seen)
1901
+ lines = split_source_lines(expanded_text)
1902
+ for index, line in enumerate(lines):
1903
+ _record_set_symbols(line.rstrip(), seen, lines, index + 1)
1903
1904
  missing = _extract_include_member(line.rstrip())
1904
1905
  if not missing:
1905
1906
  continue
@@ -26,6 +26,7 @@ import errno
26
26
  import logging
27
27
  import os
28
28
  from abc import ABC, abstractmethod
29
+ from collections.abc import Sequence
29
30
  from pathlib import Path
30
31
  from typing import NamedTuple
31
32
 
@@ -335,21 +336,22 @@ class _IncludeExpander:
335
336
  for index, line in enumerate(lines):
336
337
  expanded = None
337
338
  if index not in data_indices:
338
- expanded = self._expand_statement(line, index + 1, depth, active)
339
+ expanded = self._expand_statement(lines, index, depth, active)
339
340
  if expanded is None:
340
341
  expanded = [(line, LineOrigin(None, index + 1))]
341
342
  result.extend(expanded)
342
343
  return result
343
344
 
344
345
  def _expand_statement(
345
- self, line: str, line_number: int, depth: int, active: frozenset[str]
346
+ self, lines: list[str], index: int, depth: int, active: frozenset[str]
346
347
  ) -> list[_Entry] | None:
347
- """The expansion of *line* if it is a resolvable INCLUDE, else None."""
348
+ """The expansion of ``lines[index]`` if it is a resolvable INCLUDE."""
349
+ line_number = index + 1
348
350
  # Preserve leading whitespace: only strip the line terminator so that
349
351
  # in-stream data lines (which may contain '//' after leading spaces)
350
352
  # are never mistaken for JCL statements.
351
- statement_line = line.rstrip("\r\n")
352
- _record_set_symbols(statement_line, self.symbols)
353
+ statement_line = lines[index].rstrip("\r\n")
354
+ _record_set_symbols(statement_line, self.symbols, lines, index + 1)
353
355
  member = _extract_include_member(statement_line)
354
356
  if member is None:
355
357
  return None
@@ -413,12 +415,19 @@ def _member_origin(origin: LineOrigin, member: str, include_line: int) -> LineOr
413
415
  return LineOrigin(origin.member, include_line, origin.member_line)
414
416
 
415
417
 
416
- def _record_set_symbols(line: str, symbols: dict[str, str]) -> None:
418
+ def _record_set_symbols(
419
+ line: str,
420
+ symbols: dict[str, str],
421
+ lines: Sequence[str] = (),
422
+ follow: int = 0,
423
+ ) -> None:
417
424
  """Record the assignments of a ``// SET`` statement *line* into *symbols*.
418
425
 
419
426
  Values are resolved against the symbols already recorded, mirroring the
420
427
  parser's left-to-right SET evaluation. Lines that are not a SET statement
421
- are ignored.
428
+ are ignored. A SET whose operand field ends in a comma is continued on the
429
+ following ``//`` records: *lines* from index *follow* on (the record after
430
+ *line*) are read for its remaining assignments (#180).
422
431
  """
423
432
  if not line.startswith("//") or line.startswith("//*"):
424
433
  return
@@ -436,6 +445,7 @@ def _record_set_symbols(line: str, symbols: dict[str, str]) -> None:
436
445
  from .utils import is_positional_key, jcl_value_text
437
446
 
438
447
  statement, _ = split_comment_field(line)
448
+ statement = _continue_operands(statement, lines, follow)
439
449
  stmt = parse_statement(statement)
440
450
  if stmt is None or stmt.type != "SET":
441
451
  return
@@ -445,6 +455,28 @@ def _record_set_symbols(line: str, symbols: dict[str, str]) -> None:
445
455
  symbols[key.upper()] = _resolve_set_value(jcl_value_text(value), symbols)
446
456
 
447
457
 
458
+ def _continue_operands(statement: str, lines: Sequence[str], follow: int) -> str:
459
+ """Append the operands of the continuation records of *statement*.
460
+
461
+ A statement whose operand field ends in a comma continues on the next
462
+ ``//`` record that has a blank in column 3; its operand field (up to the
463
+ first blank) is appended. ``//*`` comment records in between are skipped.
464
+ """
465
+ from ._statement_parser import split_comment_field
466
+
467
+ statement = statement.rstrip()
468
+ while statement.endswith(",") and follow < len(lines):
469
+ cont = lines[follow].rstrip("\r\n")
470
+ follow += 1
471
+ if cont.startswith("//*"):
472
+ continue
473
+ if not cont.startswith("// ") or not cont[2:].strip():
474
+ break
475
+ operands, _ = split_comment_field("// " + cont[2:].lstrip())
476
+ statement += operands[2:].strip()
477
+ return statement
478
+
479
+
448
480
  def _substitute_member_symbols(member: str, symbols: dict[str, str]) -> str:
449
481
  """Resolve ``&SYMBOL`` references in an INCLUDE member name.
450
482
 
@@ -22,7 +22,8 @@ share a load library or DBRMLIB do not depend on one another (issue #145). The
22
22
  A DD's role comes from its ``DISP`` status: ``NEW`` / ``MOD`` write the
23
23
  dataset, ``SHR`` / ``OLD`` read it (``OLD`` may also update it in place, which
24
24
  DISP alone can't tell). A normal disposition of ``DELETE`` makes any status
25
- but ``NEW`` a read (a delete runs after the dataset's producer), and
25
+ but ``NEW`` a read (a delete runs after the dataset's producer and after
26
+ every other job that reads or writes it), and
26
27
  ``(NEW,DELETE)`` -- a scratch dataset -- neither. Temporary datasets (``&&TEMP``), ``NULLFILE``,
27
28
  ``SYSOUT`` and DDs without a ``DISP`` are left out; a ``DSN=*.STEP.DD``
28
29
  backward reference counts as the dataset it points at.
@@ -93,6 +94,9 @@ class DatasetUse:
93
94
  dataset: str
94
95
  disp: str
95
96
  line_number: int = 0
97
+ #: The DD deletes the dataset (normal disposition ``DELETE``, any status
98
+ #: but ``NEW``); it is a read that must also follow every other use.
99
+ deletes: bool = False
96
100
 
97
101
  def to_dict(self) -> dict[str, Any]:
98
102
  return {
@@ -102,6 +106,7 @@ class DatasetUse:
102
106
  "dataset": self.dataset,
103
107
  "disp": self.disp,
104
108
  "line_number": self.line_number,
109
+ "deletes": self.deletes,
105
110
  }
106
111
 
107
112
 
@@ -131,7 +136,8 @@ class DatasetLineage:
131
136
 
132
137
  @dataclass(frozen=True)
133
138
  class JobDependency:
134
- """*consumer* reads datasets that *producer* writes."""
139
+ """*consumer* reads datasets that *producer* writes, or deletes datasets
140
+ that *producer* also reads or writes (it must run after that job)."""
135
141
 
136
142
  producer: str
137
143
  consumer: str
@@ -257,6 +263,7 @@ def _record_uses(job_id: str, job: Job, datasets: dict[str, DatasetLineage]) ->
257
263
  dataset=dsn,
258
264
  disp=evt.disp_status,
259
265
  line_number=evt.line_number,
266
+ deletes=evt.disp_normal == "DELETE" and evt.disp_status != "NEW",
260
267
  )
261
268
  role = _role(evt.disp_status, evt.disp_normal)
262
269
  if role == "write":
@@ -313,12 +320,41 @@ def _dependencies(datasets: Mapping[str, DatasetLineage]) -> list[JobDependency]
313
320
  labels = edges.setdefault((producer, consumer), [])
314
321
  if label not in labels:
315
322
  labels.append(label)
323
+ _add_delete_edges(datasets, edges)
316
324
  return [
317
325
  JobDependency(producer, consumer, tuple(sorted(keys)))
318
326
  for (producer, consumer), keys in sorted(edges.items())
319
327
  ]
320
328
 
321
329
 
330
+ def _add_delete_edges(
331
+ datasets: Mapping[str, DatasetLineage], edges: dict[tuple[str, str], list[str]]
332
+ ) -> None:
333
+ """A job that deletes a dataset runs after every other job that uses it.
334
+
335
+ The producer edge is already there (a delete is a read); this adds one
336
+ from each other reader of the dataset -- or of its library or a member
337
+ of it -- so the deleter cannot be ordered before a job that still needs
338
+ the dataset (#179). Two jobs that each delete a dataset the other reads
339
+ depend on each other, a cycle.
340
+ """
341
+ for deleted, lineage in datasets.items():
342
+ deleters = _unique(u.job for u in lineage.readers if u.deletes)
343
+ if not deleters:
344
+ continue
345
+ for used, other in datasets.items():
346
+ if used != deleted and not _feeds(used, deleted):
347
+ continue
348
+ # Label the edge with the more specific name (the member).
349
+ label = deleted if deleted != library_of(deleted) else used
350
+ for user in _unique([*other.reader_jobs, *other.writer_jobs]):
351
+ for deleter in deleters:
352
+ if user != deleter:
353
+ labels = edges.setdefault((user, deleter), [])
354
+ if label not in labels:
355
+ labels.append(label)
356
+
357
+
322
358
  def _run_order(
323
359
  jobs: list[str], dependencies: list[JobDependency]
324
360
  ) -> tuple[list[str], list[str]]:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jcl-parser"
7
- version = "2.3.0"
7
+ version = "2.3.1"
8
8
  description = "Parse IBM JCL (Job Control Language) scripts into Python objects"
9
9
  authors = [
10
10
  {name = "Samuel Dion", email = "sam94dion@gmail.com"}
File without changes
File without changes
File without changes
File without changes