Skip to content

Commit 76015a6

Browse files
✨ remove duplicate classes from generated .cs files (#12)
* 🛠️ Add check for recent schema file downloads * ⚡ adding schemas path in .gitignore * ⚡️ Update .gitignore to include src/schemas/ * remove duplicates after c# code generation * removing comment blocks * first generated c# classes and removed duplicated classes * tox -e type_check * black format * fix linting errors * isort check * adding spell_check ignorance for generated folders * Update src/bo4egenerator/duplicates.py Co-authored-by: konstantin <[email protected]> * format code with black * using pathlib instead of os.path * that was just a test to check if i have enough permission --------- Co-authored-by: konstantin <[email protected]>
1 parent abb2bbe commit 76015a6

File tree

177 files changed

+152002
-10
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

177 files changed

+152002
-10
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,4 @@ dmypy.json
135135

136136
src/_your_package_version.py
137137

138-
src/bo4egenerator/schemas/
138+
src/schemas/

src/bo4egenerator/cli.py

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import typer
1111

12+
from bo4egenerator.duplicates import process_directory
1213
from bo4egenerator.generator import generate_csharp_classes
1314
from bo4egenerator.tooling import running_bo4e_schema_tool
1415

@@ -39,6 +40,9 @@ def main() -> None:
3940
# Generate C# classes
4041
generate_csharp_classes(Path(project_root), Path(schemas_dir), Path(output_dir), quicktype_executable)
4142

43+
# Remove duplicate class and enum definitions
44+
process_directory(Path(output_dir))
45+
4246

4347
def cli() -> None:
4448
"""entry point of the script defined in pyproject.toml"""

src/bo4egenerator/duplicates.py

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""
2+
Remove duplicate class and enum definitions from the generated files.
3+
"""
4+
5+
import os
6+
import re
7+
from pathlib import Path
8+
9+
10+
def find_classes_and_enums_in_file(file_path: Path) -> tuple[list[str], list[str]]:
11+
"""
12+
Find all partial class and enum definitions in a given file.
13+
14+
Args:
15+
file_path (str): The path to the file.
16+
17+
Returns:
18+
tuple: Two lists containing class names and enum names found in the file.
19+
"""
20+
class_pattern = re.compile(r"\bpublic\s+partial\s+class\s+(\w+)")
21+
enum_pattern = re.compile(r"\bpublic\s+enum\s+(\w+)")
22+
classes: list[str] = []
23+
enums: list[str] = []
24+
25+
with open(file_path, "r", encoding="utf-8") as file:
26+
content = file.read()
27+
class_matches = class_pattern.findall(content)
28+
enum_matches = enum_pattern.findall(content)
29+
classes.extend(class_matches)
30+
enums.extend(enum_matches)
31+
32+
return classes, enums
33+
34+
35+
def remove_definitions( # pylint: disable=too-many-locals, too-many-branches
36+
file_path: Path, main_class_name: str, classes_to_remove: list[str], enums_to_remove: list[str]
37+
) -> None:
38+
"""
39+
Remove specified class and enum definitions from a file, keeping the main class intact.
40+
41+
Args:
42+
file_path (str): The path to the file.
43+
main_class_name (str): The main class to keep intact.
44+
classes_to_remove (list): List of class names to remove.
45+
enums_to_remove (list): List of enum names to remove.
46+
"""
47+
try:
48+
with open(file_path, "r", encoding="utf-8") as file:
49+
lines = file.readlines()
50+
except (PermissionError, OSError) as e:
51+
print(f"Error reading file {file_path}: {e}")
52+
return
53+
54+
in_definition = False
55+
definitions = {}
56+
current_definition = None
57+
definition_start_index = 0
58+
comment_block_start_index = None
59+
60+
def find_comment_block_start(index: int) -> int:
61+
"""Find the start index of the comment block preceding the definition."""
62+
while index > 0 and re.match(r"\s*///", lines[index - 1]):
63+
index -= 1
64+
return index
65+
66+
# Read the file line by line and identify class and enum definitions
67+
for index, line in enumerate(lines):
68+
if re.match(r"\s*///", line) and not in_definition:
69+
if comment_block_start_index is None:
70+
comment_block_start_index = index
71+
continue
72+
if comment_block_start_index is not None and not re.match(r"\s*///", line):
73+
comment_block_start_index = None
74+
75+
if re.match(r"\s*public\s+partial\s+class\s+\w+", line):
76+
class_name = re.findall(r"\bpublic\s+partial\s+class\s+(\w+)", line)[0]
77+
if class_name == main_class_name:
78+
current_definition = None
79+
in_definition = False
80+
continue
81+
current_definition = class_name
82+
in_definition = True
83+
definition_start_index = find_comment_block_start(index) if comment_block_start_index is not None else index
84+
elif re.match(r"\s*public\s+enum\s+\w+", line):
85+
enum_name = re.findall(r"\bpublic\s+enum\s+(\w+)", line)[0]
86+
current_definition = enum_name
87+
in_definition = True
88+
definition_start_index = find_comment_block_start(index) if comment_block_start_index is not None else index
89+
90+
if in_definition:
91+
if re.match(r"\s*\}", line):
92+
definitions[current_definition] = (definition_start_index, index)
93+
in_definition = False
94+
current_definition = None
95+
96+
# Remove classes and enums to remove from the lines
97+
for name in classes_to_remove + enums_to_remove:
98+
if name in definitions:
99+
start, end = definitions[name]
100+
# Check for leading empty lines and comments
101+
while start > 0 and re.match(r"^\s*$", lines[start - 1]):
102+
start -= 1
103+
while start > 0 and re.match(r"\s*///", lines[start - 1]):
104+
start -= 1
105+
lines[start : end + 1] = []
106+
107+
# Write the cleaned content back to the file
108+
try:
109+
with open(file_path, "w", encoding="utf-8") as file:
110+
file.writelines(lines)
111+
except (PermissionError, OSError) as e:
112+
print(f"Error writing to file {file_path}: {e}")
113+
114+
115+
def process_directory(directory_path: Path) -> None:
116+
"""
117+
Process all files in the directory to remove duplicate class and enum definitions.
118+
Args:
119+
directory_path (Path): The path to the directory.
120+
"""
121+
for root, _, files in os.walk(directory_path):
122+
for filename in files:
123+
if filename.endswith(".cs"):
124+
file_path = Path(root) / filename
125+
class_name_from_filename = file_path.stem
126+
classes_in_file, enums_in_file = find_classes_and_enums_in_file(file_path)
127+
128+
# Exclude the main class that matches the filename
129+
classes_to_remove = [
130+
class_name for class_name in classes_in_file if class_name != class_name_from_filename
131+
]
132+
enums_to_remove = list(enums_in_file)
133+
134+
for class_name in classes_to_remove:
135+
class_file_path_bo = directory_path / "bo" / f"{class_name}.cs"
136+
class_file_path_com = directory_path / "com" / f"{class_name}.cs"
137+
138+
if class_file_path_bo.exists() or class_file_path_com.exists():
139+
print(f"Removing class {class_name} from {file_path}")
140+
remove_definitions(file_path, class_name_from_filename, [class_name], [])
141+
142+
for enum_name in enums_to_remove:
143+
enum_file_path = directory_path / "enum" / f"{enum_name}.cs"
144+
145+
if enum_file_path.exists():
146+
print(f"Removing enum {enum_name} from {file_path}")
147+
remove_definitions(file_path, class_name_from_filename, [], [enum_name])
148+
149+
print(f"Processed {file_path}")

src/bo4egenerator/tooling.py

+27-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
tooling module contains helper functions for the bo4e-generator.
33
"""
44

5+
import datetime
6+
import os
57
import subprocess
68
from pathlib import Path
79

@@ -28,7 +30,8 @@ def run_command(command: str, cwd: Path | None = None) -> subprocess.CompletedPr
2830

2931
def running_bo4e_schema_tool(schema_path: str) -> None:
3032
"""
31-
the installation step of bost shall be done at this point, because bost is a dependency of this project
33+
Checks if schema files have been downloaded in the last 30 minutes.
34+
If not, runs the bost command to download the schema files.
3235
"""
3336

3437
def _bost_is_installed() -> bool:
@@ -39,10 +42,27 @@ def _bost_is_installed() -> bool:
3942
except ImportError:
4043
return False
4144

42-
if _bost_is_installed():
43-
print("BO4E-Schema-Tool is already installed.")
44-
cli_runner = CliRunner()
45-
_ = cli_runner.invoke(main_command_line, ["-o", schema_path])
45+
def _recent_files_exist(folder: str, minutes: int) -> bool:
46+
now = datetime.datetime.now()
47+
cutoff = now - datetime.timedelta(minutes=minutes)
48+
for root, _, files in os.walk(folder):
49+
for file in files:
50+
file_path = os.path.join(root, file)
51+
file_mtime = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
52+
if file_mtime > cutoff:
53+
return True
54+
return False
55+
56+
if _recent_files_exist(schema_path, 30):
57+
print(
58+
f"BO JSON schema files in '{schema_path}' have been already downloaded in the last 30 minutes."
59+
+ "Skipping download."
60+
)
4661
else:
47-
run_command(f"bost -o {schema_path}")
48-
print("BO4E-Schema-Tool installation and schema downloading completed.")
62+
if _bost_is_installed():
63+
print("BO4E-Schema-Tool is already installed.")
64+
cli_runner = CliRunner()
65+
_ = cli_runner.invoke(main_command_line, ["-o", schema_path])
66+
else:
67+
run_command(f"bost -o {schema_path}")
68+
print("BO4E-Schema-Tool installation and schema downloading completed.")

src/dotnet-classes/ZusatzAttribut.cs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// <auto-generated />
2+
//
3+
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
4+
//
5+
// using BO4EDotNet;
6+
//
7+
// var zusatzAttribut = ZusatzAttribut.FromJson(jsonString);
8+
9+
namespace BO4EDotNet
10+
{
11+
using System;
12+
using System.Collections.Generic;
13+
14+
using System.Globalization;
15+
using Newtonsoft.Json;
16+
using Newtonsoft.Json.Converters;
17+
18+
/// <summary>
19+
/// Viele Datenobjekte weisen in unterschiedlichen Systemen eine eindeutige ID (Kundennummer,
20+
/// GP-Nummer etc.) auf.
21+
/// Beim Austausch von Datenobjekten zwischen verschiedenen Systemen ist es daher hilfreich,
22+
/// sich die eindeutigen IDs der anzubindenden Systeme zu merken.
23+
///
24+
/// .. raw:: html
25+
///
26+
/// <object data="../_static/images/bo4e/com/ZusatzAttribut.svg"
27+
/// type="image/svg+xml"></object>
28+
///
29+
/// .. HINT::
30+
/// `ZusatzAttribut JSON Schema
31+
/// <https://json-schema.app/view/%23?url=https://raw.githubusercontent.com/BO4E/BO4E-Schemas/v202401.2.1/src/bo4e_schemas/ZusatzAttribut.json>`_
32+
/// </summary>
33+
public partial class ZusatzAttribut
34+
{
35+
/// <summary>
36+
/// Bezeichnung der externen Referenz (z.B. "microservice xyz" oder "SAP CRM GP-Nummer")
37+
/// </summary>
38+
[JsonProperty("name")]
39+
public string Name { get; set; }
40+
41+
/// <summary>
42+
/// Bezeichnung der externen Referenz (z.B. "microservice xyz" oder "SAP CRM GP-Nummer")
43+
/// </summary>
44+
[JsonProperty("wert")]
45+
public object Wert { get; set; }
46+
}
47+
48+
public partial class ZusatzAttribut
49+
{
50+
public static ZusatzAttribut FromJson(string json) => JsonConvert.DeserializeObject<ZusatzAttribut>(json, BO4EDotNet.Converter.Settings);
51+
}
52+
53+
public static class Serialize
54+
{
55+
public static string ToJson(this ZusatzAttribut self) => JsonConvert.SerializeObject(self, BO4EDotNet.Converter.Settings);
56+
}
57+
58+
internal static class Converter
59+
{
60+
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
61+
{
62+
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
63+
DateParseHandling = DateParseHandling.None,
64+
Converters =
65+
{
66+
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
67+
},
68+
};
69+
}
70+
}

0 commit comments

Comments
 (0)