grimp 3.18__tar.gz → 3.19__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.
- grimp-3.19/AUTHORS.rst +16 -0
- grimp-3.19/LICENSE +21 -0
- {grimp-3.18 → grimp-3.19}/PKG-INFO +2 -3
- {grimp-3.18 → grimp-3.19}/pyproject.toml +3 -3
- {grimp-3.18 → grimp-3.19}/rust/src/graph/cycle_breakers.rs +50 -22
- {grimp-3.18 → grimp-3.19}/rust/src/graph/mod.rs +36 -21
- {grimp-3.18 → grimp-3.19}/src/grimp/__init__.py +1 -1
- {grimp-3.18 → grimp-3.19}/src/grimp/application/graph.py +10 -6
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/timing.py +2 -1
- {grimp-3.18 → grimp-3.19}/README.rst +0 -0
- {grimp-3.18 → grimp-3.19}/rust/Cargo.lock +0 -0
- {grimp-3.18 → grimp-3.19}/rust/Cargo.toml +0 -0
- {grimp-3.18 → grimp-3.19}/rust/build.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/rust-toolchain.toml +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/caching.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/errors.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/exceptions.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/filesystem.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/direct_import_queries.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/graph_manipulation.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/hierarchy_queries.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/higher_order_queries.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/import_chain_queries.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/graph/pathfinding.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/import_parsing.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/import_scanning.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/lib.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/module_expressions.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/src/module_finding.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/tests/large.rs +0 -0
- {grimp-3.18 → grimp-3.19}/rust/tests/large_graph.json +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/__init__.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/caching.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/filesystem.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/modulefinder.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/packagefinder.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/adaptors/timing.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/__init__.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/config.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/__init__.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/caching.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/filesystem.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/modulefinder.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/ports/packagefinder.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/scanning.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/application/usecases.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/domain/__init__.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/domain/analysis.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/domain/valueobjects.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/exceptions.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/main.py +0 -0
- {grimp-3.18 → grimp-3.19}/src/grimp/py.typed +0 -0
grimp-3.19/AUTHORS.rst
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
Authors
|
|
3
|
+
=======
|
|
4
|
+
|
|
5
|
+
* David Seddon - https://seddonym.me
|
|
6
|
+
* Kevin Amado - https://github.com/kamadorueda
|
|
7
|
+
* Matthew Gamble - https://github.com/mwgamble
|
|
8
|
+
* NetworkX developers - The shortest path algorithm was adapted from the NetworkX library https://networkx.org/.
|
|
9
|
+
* Peter Byfield - https://github.com/Peter554
|
|
10
|
+
* Shane Smiskol - https://github.com/sshane
|
|
11
|
+
* Andreas Rammhold - https://github.com/andir
|
|
12
|
+
* Nicholas Bunn - https://github.com/NicholasBunn
|
|
13
|
+
* Nathan McDougall - https://github.com/nathanjmcdougall
|
|
14
|
+
* Oleksandr Zaiats - https://github.com/z4y4ts
|
|
15
|
+
* Nikhil Dabas - https://github.com/ndabas
|
|
16
|
+
* Pierre-Yves Le Borgne - https://github.com/pylaterreur
|
grimp-3.19/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018, David Seddon
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
|
|
7
|
+
conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
|
10
|
+
disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
|
|
13
|
+
disclaimer in the documentation and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
16
|
+
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
17
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
18
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
|
19
|
+
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
20
|
+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
21
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grimp
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.19
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: BSD License
|
|
@@ -8,7 +8,6 @@ Classifier: Operating System :: Unix
|
|
|
8
8
|
Classifier: Operating System :: POSIX
|
|
9
9
|
Classifier: Operating System :: Microsoft :: Windows
|
|
10
10
|
Classifier: Programming Language :: Python
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -22,7 +21,7 @@ License-File: AUTHORS.rst
|
|
|
22
21
|
Summary: Builds a queryable graph of the imports within one or more Python packages.
|
|
23
22
|
Author-email: David Seddon <david@seddonym.me>
|
|
24
23
|
License: BSD 2-Clause License
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.11
|
|
26
25
|
Description-Content-Type: text/x-rst; charset=UTF-8
|
|
27
26
|
Project-URL: Documentation, https://grimp.readthedocs.io/
|
|
28
27
|
Project-URL: Source-code, https://github.com/python-grimp/grimp/
|
|
@@ -8,13 +8,13 @@ module-name = "grimp._rustgrimp"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "grimp"
|
|
11
|
-
version = "3.
|
|
11
|
+
version = "3.19"
|
|
12
12
|
license = {text = "BSD 2-Clause License"}
|
|
13
13
|
description = "Builds a queryable graph of the imports within one or more Python packages."
|
|
14
14
|
authors = [
|
|
15
15
|
{name = "David Seddon", email = "david@seddonym.me"},
|
|
16
16
|
]
|
|
17
|
-
requires-python = ">=3.
|
|
17
|
+
requires-python = ">=3.11"
|
|
18
18
|
dependencies = []
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -24,7 +24,6 @@ classifiers = [
|
|
|
24
24
|
"Operating System :: POSIX",
|
|
25
25
|
"Operating System :: Microsoft :: Windows",
|
|
26
26
|
"Programming Language :: Python",
|
|
27
|
-
"Programming Language :: Python :: 3.10",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
29
28
|
"Programming Language :: Python :: 3.12",
|
|
30
29
|
"Programming Language :: Python :: 3.13",
|
|
@@ -60,6 +59,7 @@ dev = [
|
|
|
60
59
|
"requests==2.32.3",
|
|
61
60
|
"sqlalchemy==2.0.35",
|
|
62
61
|
"google-cloud-audit-log==0.3.0",
|
|
62
|
+
"pyupgrade>=3.21.2",
|
|
63
63
|
]
|
|
64
64
|
docs = [
|
|
65
65
|
"sphinx>=7.4.7",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
use crate::errors::GrimpResult;
|
|
1
|
+
use crate::errors::{GrimpError, GrimpResult};
|
|
2
2
|
|
|
3
|
-
use crate::graph::{Graph, ModuleToken};
|
|
3
|
+
use crate::graph::{Graph, ModuleIterator, ModuleToken};
|
|
4
4
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
5
5
|
use slotmap::SecondaryMap;
|
|
6
6
|
use std::cmp::Ordering;
|
|
@@ -9,17 +9,45 @@ impl Graph {
|
|
|
9
9
|
pub fn nominate_cycle_breakers(
|
|
10
10
|
&self,
|
|
11
11
|
package: ModuleToken,
|
|
12
|
+
additional_packages: &[ModuleToken],
|
|
12
13
|
) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if additional_packages.is_empty() {
|
|
15
|
+
// A single package was passed: the nodes of the graph are its children.
|
|
16
|
+
// Children of a single package can never overlap, so no need to check.
|
|
17
|
+
let children = match self.module_children.get(package) {
|
|
18
|
+
Some(children) => children.clone(),
|
|
19
|
+
None => return Ok(FxHashSet::default()),
|
|
20
|
+
};
|
|
21
|
+
self.nominate_cycle_breakers_for_nodes(&children)
|
|
22
|
+
} else {
|
|
23
|
+
// Multiple packages were passed: the packages themselves are the nodes.
|
|
24
|
+
let mut nodes: FxHashSet<ModuleToken> = FxHashSet::default();
|
|
25
|
+
nodes.insert(package);
|
|
26
|
+
nodes.extend(additional_packages.iter().copied());
|
|
27
|
+
|
|
28
|
+
// The packages must not overlap: if one is a descendant of another, the edges
|
|
29
|
+
// between them (as packages) would be ill-defined.
|
|
30
|
+
for node in &nodes {
|
|
31
|
+
for descendant in self.get_module_descendants(*node).tokens() {
|
|
32
|
+
if nodes.contains(&descendant) {
|
|
33
|
+
return Err(GrimpError::SharedDescendants);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
self.nominate_cycle_breakers_for_nodes(&nodes)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn nominate_cycle_breakers_for_nodes(
|
|
43
|
+
&self,
|
|
44
|
+
nodes: &FxHashSet<ModuleToken>,
|
|
45
|
+
) -> GrimpResult<FxHashSet<(ModuleToken, ModuleToken)>> {
|
|
46
|
+
if nodes.len() < 2 {
|
|
19
47
|
return Ok(FxHashSet::default());
|
|
20
48
|
}
|
|
21
49
|
let (orig_imports, orig_reverse_imports, edge_weights) =
|
|
22
|
-
self.
|
|
50
|
+
self.build_graph_maps_of_nodes(nodes);
|
|
23
51
|
|
|
24
52
|
// Make a copy of the graph. We'll iteratively remove nodes from this.
|
|
25
53
|
let mut working_imports = orig_imports.clone();
|
|
@@ -166,9 +194,9 @@ impl Graph {
|
|
|
166
194
|
// with empty sets if they have no imports to / from.
|
|
167
195
|
// They are denormalized into two maps for fast lookups.
|
|
168
196
|
#[allow(clippy::type_complexity)]
|
|
169
|
-
fn
|
|
197
|
+
fn build_graph_maps_of_nodes(
|
|
170
198
|
&self,
|
|
171
|
-
|
|
199
|
+
nodes: &FxHashSet<ModuleToken>,
|
|
172
200
|
) -> (
|
|
173
201
|
SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Imports
|
|
174
202
|
SecondaryMap<ModuleToken, FxHashSet<ModuleToken>>, // Reverse imports
|
|
@@ -180,25 +208,25 @@ impl Graph {
|
|
|
180
208
|
SecondaryMap::default();
|
|
181
209
|
let mut weights = FxHashMap::default();
|
|
182
210
|
|
|
183
|
-
for
|
|
184
|
-
orig_imports.insert(*
|
|
185
|
-
orig_reverse_imports.insert(*
|
|
211
|
+
for node in nodes {
|
|
212
|
+
orig_imports.insert(*node, FxHashSet::default());
|
|
213
|
+
orig_reverse_imports.insert(*node, FxHashSet::default());
|
|
186
214
|
}
|
|
187
|
-
// Get number of imports between each
|
|
215
|
+
// Get number of imports between each node.
|
|
188
216
|
// Add as edges.
|
|
189
|
-
for
|
|
190
|
-
for
|
|
191
|
-
// Get number of imports from
|
|
217
|
+
for node_a in nodes {
|
|
218
|
+
for node_b in nodes.iter().filter(|node| *node != node_a) {
|
|
219
|
+
// Get number of imports from node_a to node_b (as package).
|
|
192
220
|
let num_imports_from_a_to_b = self
|
|
193
|
-
.find_direct_imports_between(*
|
|
221
|
+
.find_direct_imports_between(*node_a, *node_b, true)
|
|
194
222
|
.unwrap()
|
|
195
223
|
.len();
|
|
196
224
|
|
|
197
225
|
if num_imports_from_a_to_b > 0 {
|
|
198
226
|
// a depends on b - add it as an edge in our temporary graph.
|
|
199
|
-
orig_imports[*
|
|
200
|
-
orig_reverse_imports[*
|
|
201
|
-
weights.insert((*
|
|
227
|
+
orig_imports[*node_a].insert(*node_b);
|
|
228
|
+
orig_reverse_imports[*node_b].insert(*node_a);
|
|
229
|
+
weights.insert((*node_a, *node_b), num_imports_from_a_to_b);
|
|
202
230
|
}
|
|
203
231
|
}
|
|
204
232
|
}
|
|
@@ -198,6 +198,33 @@ impl GraphWrapper {
|
|
|
198
198
|
|
|
199
199
|
PyTuple::new(py, python_dependencies)
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
fn convert_cycle_breakers_to_python<'py>(
|
|
203
|
+
&self,
|
|
204
|
+
py: Python<'py>,
|
|
205
|
+
cycle_breakers: FxHashSet<(ModuleToken, ModuleToken)>,
|
|
206
|
+
) -> PyResult<Bound<'py, PySet>> {
|
|
207
|
+
PySet::new(
|
|
208
|
+
py,
|
|
209
|
+
cycle_breakers
|
|
210
|
+
.into_iter()
|
|
211
|
+
.map(|(importer, imported)| {
|
|
212
|
+
let importer = self._graph.get_module(importer).unwrap();
|
|
213
|
+
let imported = self._graph.get_module(imported).unwrap();
|
|
214
|
+
Import::new(importer.name(), imported.name())
|
|
215
|
+
})
|
|
216
|
+
.map(|import| {
|
|
217
|
+
PyTuple::new(
|
|
218
|
+
py,
|
|
219
|
+
[
|
|
220
|
+
import.importer.into_py_any(py).unwrap(),
|
|
221
|
+
import.imported.into_py_any(py).unwrap(),
|
|
222
|
+
],
|
|
223
|
+
)
|
|
224
|
+
.unwrap()
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
201
228
|
}
|
|
202
229
|
|
|
203
230
|
/// Wrapper around the Graph struct that integrates with Python.
|
|
@@ -642,29 +669,17 @@ impl GraphWrapper {
|
|
|
642
669
|
&self,
|
|
643
670
|
py: Python<'py>,
|
|
644
671
|
package: &str,
|
|
672
|
+
additional_packages: Vec<String>,
|
|
645
673
|
) -> PyResult<Bound<'py, PySet>> {
|
|
646
674
|
let package = self.get_visible_module_by_name(package)?.token();
|
|
647
|
-
let
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
Import::new(importer.name(), imported.name())
|
|
656
|
-
})
|
|
657
|
-
.map(|import| {
|
|
658
|
-
PyTuple::new(
|
|
659
|
-
py,
|
|
660
|
-
[
|
|
661
|
-
import.importer.into_py_any(py).unwrap(),
|
|
662
|
-
import.imported.into_py_any(py).unwrap(),
|
|
663
|
-
],
|
|
664
|
-
)
|
|
665
|
-
.unwrap()
|
|
666
|
-
}),
|
|
667
|
-
)
|
|
675
|
+
let additional_packages = additional_packages
|
|
676
|
+
.iter()
|
|
677
|
+
.map(|package| Ok(self.get_visible_module_by_name(package)?.token()))
|
|
678
|
+
.collect::<PyResult<Vec<_>>>()?;
|
|
679
|
+
let cycle_breakers = self
|
|
680
|
+
._graph
|
|
681
|
+
.nominate_cycle_breakers(package, &additional_packages)?;
|
|
682
|
+
self.convert_cycle_breakers_to_python(py, cycle_breakers)
|
|
668
683
|
}
|
|
669
684
|
|
|
670
685
|
#[pyo3(name = "clone")]
|
|
@@ -476,14 +476,18 @@ class ImportGraph:
|
|
|
476
476
|
|
|
477
477
|
return _dependencies_from_tuple(result)
|
|
478
478
|
|
|
479
|
-
def nominate_cycle_breakers(self, package: str) -> set[ImportTuple]:
|
|
479
|
+
def nominate_cycle_breakers(self, package: str, *additional_packages: str) -> set[ImportTuple]:
|
|
480
480
|
"""
|
|
481
|
-
Identify a set of imports that, if removed, would make the
|
|
481
|
+
Identify a set of imports that, if removed, would make the supplied packages acyclic.
|
|
482
|
+
|
|
483
|
+
For more information see the docs in usage.rst.
|
|
482
484
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
for package_ in (package, *additional_packages):
|
|
486
|
+
if not self._rustgraph.contains_module(package_):
|
|
487
|
+
msg = f'"{package_}" not present in the graph.'
|
|
488
|
+
raise ModuleNotPresent(msg)
|
|
489
|
+
|
|
490
|
+
return self._rustgraph.nominate_cycle_breakers(package, additional_packages)
|
|
487
491
|
|
|
488
492
|
# Dunder methods
|
|
489
493
|
# --------------
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
4
|
from types import TracebackType
|
|
5
|
+
from typing import Self
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class Timer(abc.ABC):
|
|
@@ -22,7 +23,7 @@ class Timer(abc.ABC):
|
|
|
22
23
|
# We use a stack so context managers can be nested.
|
|
23
24
|
self._start_stack: list[float] = []
|
|
24
25
|
|
|
25
|
-
def __enter__(self) ->
|
|
26
|
+
def __enter__(self) -> Self:
|
|
26
27
|
self._start_stack.append(self.get_current_time())
|
|
27
28
|
return self
|
|
28
29
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|