Skip to content

XsCheck

Source code in AoE2ScenarioParser/objects/support/xs_check.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
class XsCheck:
    version: Tuple[int, int, int] = (0, 2, 1)

    def __init__(self, uuid: UUID):
        self._uuid: UUID = uuid

        self.enabled = True
        """If XS-Check is enabled or not"""
        self.xs_encoding = 'utf-8'
        """The encoding that should be used for reading and writing the file with the XS script"""
        self.allow_unsupported_versions: bool = False
        """If XS-Check should be checked for compatibility"""
        self.raise_on_error: bool = False
        """If a Python error should be raised if XS-Check encounters an error"""

        self.path = None
        """The path for a custom XS check binary (One that is not shipped with AoE2ScenarioParser"""

        self.timeout_seconds = 60
        """The timeout for the XS-Check call in seconds"""

        self._default_folder = Path(__file__).parent.parent.parent / 'dependencies' / 'xs-check'
        """The folder path to look for the XS check binaries (shipped with AoE2ScenarioParser)"""

    @property
    def is_enabled(self):
        return self.enabled and settings.ENABLE_XS_CHECK_INTEGRATION

    @property
    def is_disabled(self):
        return not self.is_enabled

    @property
    def path(self) -> Optional[Path]:
        """The path for a custom XS check binary (One that is not shipped with AoE2ScenarioParser"""
        if self._path is None:
            if os.name == 'nt':
                extension = '.exe'
            elif os.name == 'posix':
                extension = ''
            else:
                raise Exception('Unsupported platform')

            return self._default_folder / ('xs-check' + extension)

        return self._path

    @path.setter
    def path(self, value: Optional[Union[Path, str]]):
        if value is None:
            self._path = None
            return

        path = value if isinstance(value, Path) else Path(value)
        if not path.is_file():
            raise ValueError(f'Unable to find xs-check binary at "{path}"')

        self._path = path

        if not self.is_supported_xs_check_binary():
            self._raise_unsupported()

    def validate(self, xs_file: Optional[Union[Path, str]], show_tmpfile: bool = True) -> True:
        """
        Validates the XS file and throws an exception if xs-check finds an error

        Args:
            xs_file: The XS file to validate
            show_tmpfile: If a reference to the tmp file should be displayed

        Throws:
            XsCheckValidationError: When xs-check encounters an error

        Returns:
            True if no errors are found or xs-check has been disabled, an ``XsCheckValidationError`` is thrown otherwise
        """
        if self.is_disabled:
            return True

        if not self.is_supported_xs_check_binary():
            self._raise_unsupported()

        xs_file_path = str(xs_file.absolute()) if isinstance(xs_file, Path) else xs_file

        output = self._call(xs_file_path)

        if output.startswith('No errors found in file'):
            return True

        # Do not show temp file name as it might be confusing
        output = output.replace(xs_file_path, 'AoE2ScenarioParser.xs')

        version = '.'.join(str(v) for v in self.get_version())

        s_print('\n' + ('-' * 25) + '<[ XS-CHECK VALIDATION RESULT ]>' + ('-' * 25), final=True)

        s_print(f"\nxs-check:{version} output: [ Provided by: https://github.com/Divy1211/xs-check/ ]\n", final=True)
        s_print(add_tabs(output, 1), final=True)

        self._print_parsed_xs_check_errors(output)

        if show_tmpfile:
            display_path = xs_file_path.replace('\\', '/')
            s_print(f"\nOpen the file below to view the entire XS file:\n\tfile:///{display_path}", final=True)

        time.sleep(.5)

        if self.raise_on_error:
            raise XsCheckValidationError("Xs-Check failed validation, see errors above", xs_check_errors=output)

        s_print('\n' + ('-' * 25) + '<[ END XS-CHECK VALIDATION RESULT ]>' + ('-' * 25), final=True)

    def validate_safe(self, xs_file: Optional[Union[Path, str]]) -> bool:
        """
        Validates the XS file and and returns a boolean based on if errors were found

        Args:
            xs_file: The XS file to validate

        Returns:
            True if no errors are found or xs-check has been disabled, False otherwise
        """
        try:
            return self.validate(xs_file)
        except XsCheckValidationError:
            return False

    def is_supported_xs_check_binary(self) -> bool:
        """
        Check if the xs-check binary is supported by this version of AoE2ScenarioParser.
        Allows an override to easily allow users to use newer versions which might be supported too.

        Returns:
            True if it is supported, False otherwise
        """
        if self.allow_unsupported_versions:
            return True

        version_tuple = self.get_version()

        if (0, 1, 2) <= version_tuple <= (0, 2, 1):
            return True

        return False

    def get_version(self) -> Tuple[int, ...]:
        """
        Get the version from the xs-check binary

        Returns:
            A tuple containing the version xs-check number
        """
        stdout = self._call('-v')

        version_string = self._get_version_from_xs_check_v_string(stdout)

        # Make it a proper version tuple
        return tuple(map(int, version_string.split('.')))

    def _get_version_from_xs_check_v_string(self, stdout: str) -> str:
        """
        Use a regex match to find the xs-check version from the string

        Args:
            stdout: The STDOUT from a ``xs-check -v`` call

        Returns:
            The xs-check version
        """

        def is_match(result: re.Match) -> bool:
            return result is not None and len(result.groups()) == 1

        if is_match(result := re.match(r'xs-check v(\d+\.\d+\.\d+):', stdout)):
            return result.groups()[0]

        raise ValueError(f'Unable to locate version from xs-check string: "{stdout}"')

    def _call(self, *args: str):
        """
        Call XS Check

        Args:
            *args: The arguments to be added to xs-check

        Returns:
            The STDOUT from the call as a string
        """
        # Make temp files to capture xs-check output
        stdout_file, stdout_path = tempfile.mkstemp()
        stderr_file, stderr_path = tempfile.mkstemp()

        command = [self.path, *args]
        exitcode = subprocess.call(command, timeout=self.timeout_seconds, stdout=stdout_file, stderr=stderr_file)

        if exitcode != 0:
            error = Path(stderr_path).read_text(encoding=self.xs_encoding)

            raise ValueError(f"A non-zero exit code ({exitcode}) was returned by xs-check: '{error}'")

        return Path(stdout_path).read_text(encoding=self.xs_encoding)

    def _print_parsed_xs_check_errors(self, output: str) -> None:
        # Remove unwanted characters from output (Color highlighting etc.)
        plain_output = re.sub(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])', '', output)

        prev_trigger_index = None
        matches = re.findall(r'/\*T(\d+)([CE])(\d+)\*/', plain_output)

        if len(matches) == 0:
            return

        s_print(f"\nXS-Check errors origins:\n", final=True)
        for match in matches:
            trigger_index, ce_type, ce_index = match

            trigger = getters.get_trigger(self._uuid, int(trigger_index))
            if ce_type == 'C':
                obj = 'Condition'
                type_ = trigger.conditions[int(ce_index)].condition_type
                obj_name = pretty_format_name(ConditionId(type_).name)
            elif ce_type == 'E':
                obj = 'Effect'
                type_ = trigger.effects[int(ce_index)].effect_type
                obj_name = pretty_format_name(EffectId(type_).name)
            else:
                continue

            if prev_trigger_index != trigger_index:
                s_print(f"  ⇒ [Trigger #{trigger_index}] '{trigger.name}'", final=True)

            s_print(f"     ↳ [{obj} #{ce_index}] {obj_name} {obj}", final=True)
        s_print(f"", final=True)

    def _raise_unsupported(self):
        version = '.'.join(str(v) for v in self.get_version())
        raise ValueError(
            f'Unsupported xs-check binary given with version "{version}" at "{self._path}". \n'
            f'You can try `xs_manager.xs_check.allow_unsupported_versions = True` to override this check'
        )

Attributes

allow_unsupported_versions: bool = False instance-attribute

Type: bool
Value: False

If XS-Check should be checked for compatibility

enabled = True instance-attribute

Value: True

If XS-Check is enabled or not

is_disabled property

is_enabled property

path: Optional[Path] property writable

Type: Optional[Path]

The path for a custom XS check binary (One that is not shipped with AoE2ScenarioParser

raise_on_error: bool = False instance-attribute

Type: bool
Value: False

If a Python error should be raised if XS-Check encounters an error

timeout_seconds = 60 instance-attribute

Value: 60

The timeout for the XS-Check call in seconds

version: Tuple[int, int, int] = (0, 2, 1) class-attribute instance-attribute

Type: Tuple[int, int, int]
Value: (0, 2, 1)

xs_encoding = 'utf-8' instance-attribute

Value: 'utf-8'

The encoding that should be used for reading and writing the file with the XS script

Functions


def __init__(...)

Parameters:

Name Type Description Default
uuid UUID - required
Source code in AoE2ScenarioParser/objects/support/xs_check.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def __init__(self, uuid: UUID):
    self._uuid: UUID = uuid

    self.enabled = True
    """If XS-Check is enabled or not"""
    self.xs_encoding = 'utf-8'
    """The encoding that should be used for reading and writing the file with the XS script"""
    self.allow_unsupported_versions: bool = False
    """If XS-Check should be checked for compatibility"""
    self.raise_on_error: bool = False
    """If a Python error should be raised if XS-Check encounters an error"""

    self.path = None
    """The path for a custom XS check binary (One that is not shipped with AoE2ScenarioParser"""

    self.timeout_seconds = 60
    """The timeout for the XS-Check call in seconds"""

    self._default_folder = Path(__file__).parent.parent.parent / 'dependencies' / 'xs-check'
    """The folder path to look for the XS check binaries (shipped with AoE2ScenarioParser)"""

def get_version(...)

Get the version from the xs-check binary

Returns:

Type Description
Tuple[int, ...]

A tuple containing the version xs-check number

Source code in AoE2ScenarioParser/objects/support/xs_check.py
165
166
167
168
169
170
171
172
173
174
175
176
177
def get_version(self) -> Tuple[int, ...]:
    """
    Get the version from the xs-check binary

    Returns:
        A tuple containing the version xs-check number
    """
    stdout = self._call('-v')

    version_string = self._get_version_from_xs_check_v_string(stdout)

    # Make it a proper version tuple
    return tuple(map(int, version_string.split('.')))

def is_supported_xs_check_binary(...)

Check if the xs-check binary is supported by this version of AoE2ScenarioParser. Allows an override to easily allow users to use newer versions which might be supported too.

Returns:

Type Description
bool

True if it is supported, False otherwise

Source code in AoE2ScenarioParser/objects/support/xs_check.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def is_supported_xs_check_binary(self) -> bool:
    """
    Check if the xs-check binary is supported by this version of AoE2ScenarioParser.
    Allows an override to easily allow users to use newer versions which might be supported too.

    Returns:
        True if it is supported, False otherwise
    """
    if self.allow_unsupported_versions:
        return True

    version_tuple = self.get_version()

    if (0, 1, 2) <= version_tuple <= (0, 2, 1):
        return True

    return False

def validate(...)

Validates the XS file and throws an exception if xs-check finds an error

Parameters:

Name Type Description Default
xs_file Optional[Union[Path, str]]

The XS file to validate

required
show_tmpfile bool

If a reference to the tmp file should be displayed

True
Throws

XsCheckValidationError: When xs-check encounters an error

Returns:

Type Description
True

True if no errors are found or xs-check has been disabled, an XsCheckValidationError is thrown otherwise

Source code in AoE2ScenarioParser/objects/support/xs_check.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def validate(self, xs_file: Optional[Union[Path, str]], show_tmpfile: bool = True) -> True:
    """
    Validates the XS file and throws an exception if xs-check finds an error

    Args:
        xs_file: The XS file to validate
        show_tmpfile: If a reference to the tmp file should be displayed

    Throws:
        XsCheckValidationError: When xs-check encounters an error

    Returns:
        True if no errors are found or xs-check has been disabled, an ``XsCheckValidationError`` is thrown otherwise
    """
    if self.is_disabled:
        return True

    if not self.is_supported_xs_check_binary():
        self._raise_unsupported()

    xs_file_path = str(xs_file.absolute()) if isinstance(xs_file, Path) else xs_file

    output = self._call(xs_file_path)

    if output.startswith('No errors found in file'):
        return True

    # Do not show temp file name as it might be confusing
    output = output.replace(xs_file_path, 'AoE2ScenarioParser.xs')

    version = '.'.join(str(v) for v in self.get_version())

    s_print('\n' + ('-' * 25) + '<[ XS-CHECK VALIDATION RESULT ]>' + ('-' * 25), final=True)

    s_print(f"\nxs-check:{version} output: [ Provided by: https://github.com/Divy1211/xs-check/ ]\n", final=True)
    s_print(add_tabs(output, 1), final=True)

    self._print_parsed_xs_check_errors(output)

    if show_tmpfile:
        display_path = xs_file_path.replace('\\', '/')
        s_print(f"\nOpen the file below to view the entire XS file:\n\tfile:///{display_path}", final=True)

    time.sleep(.5)

    if self.raise_on_error:
        raise XsCheckValidationError("Xs-Check failed validation, see errors above", xs_check_errors=output)

    s_print('\n' + ('-' * 25) + '<[ END XS-CHECK VALIDATION RESULT ]>' + ('-' * 25), final=True)

def validate_safe(...)

Validates the XS file and and returns a boolean based on if errors were found

Parameters:

Name Type Description Default
xs_file Optional[Union[Path, str]]

The XS file to validate

required

Returns:

Type Description
bool

True if no errors are found or xs-check has been disabled, False otherwise

Source code in AoE2ScenarioParser/objects/support/xs_check.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def validate_safe(self, xs_file: Optional[Union[Path, str]]) -> bool:
    """
    Validates the XS file and and returns a boolean based on if errors were found

    Args:
        xs_file: The XS file to validate

    Returns:
        True if no errors are found or xs-check has been disabled, False otherwise
    """
    try:
        return self.validate(xs_file)
    except XsCheckValidationError:
        return False