pycodedj 0.6.0__tar.gz → 0.6.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 (39) hide show
  1. {pycodedj-0.6.0 → pycodedj-0.6.1}/CHANGELOG.md +10 -0
  2. {pycodedj-0.6.0 → pycodedj-0.6.1}/PKG-INFO +1 -1
  3. {pycodedj-0.6.0 → pycodedj-0.6.1}/README.ja.md +1 -1
  4. pycodedj-0.6.1/docs/favicon.svg +17 -0
  5. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/index.html +11 -1
  6. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/manual.html +13 -3
  7. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/manual.ja.html +12 -2
  8. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/manual.ja.md +2 -2
  9. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/manual.md +3 -3
  10. {pycodedj-0.6.0 → pycodedj-0.6.1}/pyproject.toml +1 -1
  11. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/__init__.py +1 -1
  12. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/__main__.py +1 -1
  13. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/watcher.py +12 -5
  14. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_watcher.py +36 -0
  15. {pycodedj-0.6.0 → pycodedj-0.6.1}/.github/workflows/workflow.yml +0 -0
  16. {pycodedj-0.6.0 → pycodedj-0.6.1}/.gitignore +0 -0
  17. {pycodedj-0.6.0 → pycodedj-0.6.1}/LICENSE +0 -0
  18. {pycodedj-0.6.0 → pycodedj-0.6.1}/README.md +0 -0
  19. {pycodedj-0.6.0 → pycodedj-0.6.1}/docs/CNAME +0 -0
  20. {pycodedj-0.6.0 → pycodedj-0.6.1}/examples/club_set.py +0 -0
  21. {pycodedj-0.6.0 → pycodedj-0.6.1}/examples/demo.py +0 -0
  22. {pycodedj-0.6.0 → pycodedj-0.6.1}/examples/hello_sc.py +0 -0
  23. {pycodedj-0.6.0 → pycodedj-0.6.1}/examples/sound_showcase.py +0 -0
  24. {pycodedj-0.6.0 → pycodedj-0.6.1}/sc/synths.scd +0 -0
  25. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/_loop.py +0 -0
  26. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/analyzer.py +0 -0
  27. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/block_parser.py +0 -0
  28. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/engine.py +0 -0
  29. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/mapper.py +0 -0
  30. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/osc_bridge.py +0 -0
  31. {pycodedj-0.6.0 → pycodedj-0.6.1}/src/pycodedj/pattern.py +0 -0
  32. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/__init__.py +0 -0
  33. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_analyzer.py +0 -0
  34. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_block_parser.py +0 -0
  35. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_cli.py +0 -0
  36. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_engine.py +0 -0
  37. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_mapper.py +0 -0
  38. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_osc_bridge.py +0 -0
  39. {pycodedj-0.6.0 → pycodedj-0.6.1}/tests/test_pattern.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1] - 2026-05-09
4
+
5
+ ### Improvements
6
+
7
+ - `pycodedj watch` が保存時に変更されたループだけを再評価するように変更。未変更のループは OSC 再送信されず、パターンの不要な再起動感を抑える
8
+
9
+ ### Docs
10
+
11
+ - watch モードの説明を、起動時は全ループ評価・保存時は変更ループのみ再評価する仕様に更新
12
+
3
13
  ## [0.6.0] - 2026-05-08
4
14
 
5
15
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycodedj
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: A live-coding performance tool that turns Python code structure into music via SuperCollider.
5
5
  License: MIT License with Commons Clause
6
6
 
@@ -118,7 +118,7 @@ def pad(volume=0.1):
118
118
  pycodedj watch demo.py
119
119
  ```
120
120
 
121
- あとはエディタでコードを書いて保存するだけです。保存のたびに全ループが再評価されます。
121
+ あとはエディタでコードを書いて保存するだけです。保存のたびに変更されたループだけが再評価されます。
122
122
 
123
123
  **4. 緊急停止**
124
124
 
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="PyCodeDJ">
2
+ <defs>
3
+ <linearGradient id="bg" x1="10" y1="8" x2="54" y2="58" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#16161f"/>
5
+ <stop offset="1" stop-color="#08080e"/>
6
+ </linearGradient>
7
+ <linearGradient id="wave" x1="14" y1="32" x2="50" y2="32" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#a6e3a1"/>
9
+ <stop offset="0.52" stop-color="#89b4fa"/>
10
+ <stop offset="1" stop-color="#cba6f7"/>
11
+ </linearGradient>
12
+ </defs>
13
+ <rect width="64" height="64" rx="14" fill="url(#bg)"/>
14
+ <rect x="8" y="8" width="48" height="48" rx="10" fill="none" stroke="#1e1e2e" stroke-width="2"/>
15
+ <path d="M15 32c4-12 8-12 12 0s8 12 12 0 8-12 12 0" fill="none" stroke="url(#wave)" stroke-width="5" stroke-linecap="round"/>
16
+ <circle cx="48" cy="18" r="4" fill="#f38ba8"/>
17
+ </svg>
@@ -4,6 +4,16 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>PyCodeDJ — Code is the Instrument</title>
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
8
+ <!-- Google tag (gtag.js) -->
9
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-J87606964E"></script>
10
+ <script>
11
+ window.dataLayer = window.dataLayer || [];
12
+ function gtag(){dataLayer.push(arguments);}
13
+ gtag('js', new Date());
14
+
15
+ gtag('config', 'G-J87606964E');
16
+ </script>
7
17
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
18
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
19
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,600;1,400&family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
@@ -533,7 +543,7 @@
533
543
 
534
544
  <!-- Hero -->
535
545
  <section class="hero">
536
- <div class="hero-badge">v0.1.1 — now on PyPI</div>
546
+ <div class="hero-badge">v0.6.0 — now on PyPI</div>
537
547
  <h1>
538
548
  <span class="accent-green">Code</span> is<br>
539
549
  the <span class="accent-mauve">instrument</span>
@@ -4,6 +4,16 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>PyCodeDJ Manual</title>
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
8
+ <!-- Google tag (gtag.js) -->
9
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-J87606964E"></script>
10
+ <script>
11
+ window.dataLayer = window.dataLayer || [];
12
+ function gtag(){dataLayer.push(arguments);}
13
+ gtag('js', new Date());
14
+
15
+ gtag('config', 'G-J87606964E');
16
+ </script>
7
17
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
18
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
19
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,600;1,400&family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
@@ -503,12 +513,12 @@ def bass(volume=0.4):
503
513
  <hr>
504
514
  </section>
505
515
  <section id="s5"><h2>5. Watch mode &mdash; save to reload</h2>
506
- <p>Running <code>pycodedj eval</code> manually each time gets tedious. The <code>watch</code> command evaluates all loops at startup and then <strong>re-evaluates automatically on every save</strong>.</p>
516
+ <p>Running <code>pycodedj eval</code> manually each time gets tedious. The <code>watch</code> command evaluates all loops at startup and then <strong>re-evaluates only changed loops on every save</strong>.</p>
507
517
  <h3>Start watching</h3>
508
518
  <pre data-lang="bash"><code>pycodedj watch examples/demo.py</code></pre>
509
519
  <pre data-lang="text"><code>[pycodedj] watching demo.py — save to reload (Ctrl+C to stop)
510
520
  [pycodedj] reloaded demo.py (3 loop(s))</code></pre>
511
- <p>All loops play immediately. From here, just write code and save. Every save shows:</p>
521
+ <p>All loops play immediately. From here, just write code and save. Unchanged loops keep running without being resent. Every save shows:</p>
512
522
  <pre data-lang="text"><code>[pycodedj] reloaded demo.py (3 loop(s))</code></pre>
513
523
  <p><strong>Write code → Save → Sound changes.</strong> That&#x27;s the live-coding workflow.</p>
514
524
  <h3>Stop watching</h3>
@@ -1349,7 +1359,7 @@ s.boot;</code></pre>
1349
1359
  pycodedj eval demo.py::melody --sc-host 192.168.1.10
1350
1360
  pycodedj eval demo.py::pad --sc-port 57200</code></pre>
1351
1361
  <h3><code>pycodedj watch</code></h3>
1352
- <p>Watch a file and re-evaluate all loops at startup and on every save. Stop with Ctrl+C.</p>
1362
+ <p>Watch a file, evaluate all loops at startup, and re-evaluate changed loops on every save. Stop with Ctrl+C.</p>
1353
1363
  <pre data-lang="text"><code>pycodedj watch FILE [--sc-host HOST] [--sc-port PORT] [--debounce SECS]</code></pre>
1354
1364
  <table>
1355
1365
  <thead><tr>
@@ -4,6 +4,16 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>PyCodeDJ マニュアル</title>
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml">
8
+ <!-- Google tag (gtag.js) -->
9
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-J87606964E"></script>
10
+ <script>
11
+ window.dataLayer = window.dataLayer || [];
12
+ function gtag(){dataLayer.push(arguments);}
13
+ gtag('js', new Date());
14
+
15
+ gtag('config', 'G-J87606964E');
16
+ </script>
7
17
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
18
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
19
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,600;1,400&family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
@@ -601,7 +611,7 @@ def bass(volume=0.7): # ← 大きくする
601
611
  <section id="s5">
602
612
  <h2>5. 保存するだけで音が変わる — watch モード</h2>
603
613
 
604
- <p>毎回 <code>pycodedj eval</code> を打つのは面倒です。<code>watch</code> コマンドを使うと、起動時に一度すべてのループを評価し、その後は<strong>ファイルを保存するだけで自動的に全ループが再評価されます</strong>。</p>
614
+ <p>毎回 <code>pycodedj eval</code> を打つのは面倒です。<code>watch</code> コマンドを使うと、起動時に一度すべてのループを評価し、その後は<strong>ファイルを保存するだけで変更されたループだけが再評価されます</strong>。</p>
605
615
 
606
616
  <h3>起動方法</h3>
607
617
 
@@ -610,7 +620,7 @@ def bass(volume=0.7): # ← 大きくする
610
620
  <pre data-lang="text"><code>[pycodedj] watching demo.py — save to reload (Ctrl+C to stop)
611
621
  [pycodedj] reloaded demo.py (3 loop(s))</code></pre>
612
622
 
613
- <p>起動直後に一度音が鳴ります。あとはエディタでコードを書いて保存するだけです。保存のたびに次のように出力されます。</p>
623
+ <p>起動直後に一度音が鳴ります。あとはエディタでコードを書いて保存するだけです。変更していないループは再送信されず、そのまま鳴り続けます。保存のたびに次のように出力されます。</p>
614
624
 
615
625
  <pre data-lang="text"><code>[pycodedj] reloaded demo.py (3 loop(s))</code></pre>
616
626
 
@@ -318,7 +318,7 @@ def bass(volume=0.7): # 大きくする
318
318
 
319
319
  ## 5. watch モード — 保存するだけで音が変わる
320
320
 
321
- 毎回 `pycodedj eval` を打つのは手間です。`watch` コマンドを使うと、起動時に全ループをまとめて評価し、その後は**ファイルを保存するだけで自動的に全ループが再評価**されます。
321
+ 毎回 `pycodedj eval` を打つのは手間です。`watch` コマンドを使うと、起動時に全ループをまとめて評価し、その後は**ファイルを保存するだけで変更されたループだけが再評価**されます。
322
322
 
323
323
  ### 起動する
324
324
 
@@ -331,7 +331,7 @@ pycodedj watch examples/demo.py
331
331
  [pycodedj] reloaded demo.py (3 loop(s))
332
332
  ```
333
333
 
334
- 起動直後に一度すべてのループが評価されて音が鳴ります。あとはエディタでコードを書いて保存するだけです。
334
+ 起動直後に一度すべてのループが評価されて音が鳴ります。あとはエディタでコードを書いて保存するだけです。変更していないループは再送信されず、そのまま鳴り続けます。
335
335
 
336
336
  ```
337
337
  [pycodedj] reloaded demo.py (3 loop(s))
@@ -301,7 +301,7 @@ The filter opened up and the modulation got faster. Deeper nesting = brighter so
301
301
 
302
302
  ## 5. Watch mode — save to reload
303
303
 
304
- Running `pycodedj eval` manually each time gets tedious. The `watch` command evaluates all loops at startup and then **re-evaluates automatically on every save**.
304
+ Running `pycodedj eval` manually each time gets tedious. The `watch` command evaluates all loops at startup and then **re-evaluates only changed loops on every save**.
305
305
 
306
306
  ### Start watching
307
307
 
@@ -314,7 +314,7 @@ pycodedj watch examples/demo.py
314
314
  [pycodedj] reloaded demo.py (3 loop(s))
315
315
  ```
316
316
 
317
- All loops play immediately. From here, just write code and save. Every save shows:
317
+ All loops play immediately. From here, just write code and save. Unchanged loops keep running without being resent. Every save shows:
318
318
 
319
319
  ```
320
320
  [pycodedj] reloaded demo.py (3 loop(s))
@@ -1081,7 +1081,7 @@ pycodedj eval demo.py::pad --sc-port 57200
1081
1081
 
1082
1082
  ### `pycodedj watch`
1083
1083
 
1084
- Watch a file and re-evaluate all loops at startup and on every save. Stop with Ctrl+C.
1084
+ Watch a file, evaluate all loops at startup, and re-evaluate changed loops on every save. Stop with Ctrl+C.
1085
1085
 
1086
1086
  ```
1087
1087
  pycodedj watch FILE [--sc-host HOST] [--sc-port PORT] [--debounce SECS]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pycodedj"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  description = "A live-coding performance tool that turns Python code structure into music via SuperCollider."
5
5
  requires-python = ">=3.10"
6
6
  readme = "README.md"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from ._loop import loop, pattern
4
4
 
5
- __version__ = "0.6.0"
5
+ __version__ = "0.6.1"
6
6
  __all__ = ["loop", "pattern"]
@@ -24,7 +24,7 @@ def _build_parser() -> argparse.ArgumentParser:
24
24
  eval_p.add_argument("--sc-host", default="127.0.0.1", help="SuperCollider host")
25
25
  eval_p.add_argument("--sc-port", default=57120, type=int, help="SuperCollider port")
26
26
 
27
- watch_p = sub.add_parser("watch", help="Watch a file and re-eval all loops on save")
27
+ watch_p = sub.add_parser("watch", help="Watch a file and re-eval changed loops on save")
28
28
  watch_p.add_argument("file", metavar="FILE", help="Source file to watch")
29
29
  watch_p.add_argument("--sc-host", default="127.0.0.1", help="SuperCollider host")
30
30
  watch_p.add_argument("--sc-port", default=57120, type=int, help="SuperCollider port")
@@ -6,13 +6,13 @@ import threading
6
6
  from pathlib import Path
7
7
  from typing import Callable
8
8
 
9
- from .block_parser import parse_blocks
9
+ from .block_parser import LoopBlock, parse_blocks
10
10
  from .engine import Engine
11
11
  from .osc_bridge import OscError
12
12
 
13
13
 
14
14
  class _LoopFileHandler:
15
- """ファイル変更イベントを受け取り、デバウンス後に全ループを再評価する。"""
15
+ """ファイル変更イベントを受け取り、デバウンス後に変更ループを再評価する。"""
16
16
 
17
17
  def __init__(
18
18
  self,
@@ -27,6 +27,7 @@ class _LoopFileHandler:
27
27
  self._debounce = debounce
28
28
  self._timer: threading.Timer | None = None
29
29
  self._active_names: set[str] = set()
30
+ self._blocks_by_name: dict[str, LoopBlock] = {}
30
31
 
31
32
  def dispatch(self, src_path: str) -> None:
32
33
  """watchdog の on_modified から呼ぶ。対象ファイル以外は無視する。"""
@@ -50,17 +51,23 @@ class _LoopFileHandler:
50
51
  if not result.ok:
51
52
  sys.stderr.write(f"[pycodedj] syntax error in {self._path}: {result.error}\n")
52
53
  return # _active_names を維持してループを継続する
53
- current_names = {b.name for b in result.blocks}
54
+ current_blocks = {b.name: b for b in result.blocks}
55
+ current_names = set(current_blocks)
54
56
 
55
57
  for name in self._active_names - current_names:
56
58
  try:
57
59
  self._engine.stop_loop(name)
58
60
  except OscError:
59
61
  pass
62
+ self._blocks_by_name.pop(name, None)
60
63
  self._active_names = current_names
61
64
 
62
65
  for block in result.blocks:
63
- self._engine.eval_block(block)
66
+ if self._blocks_by_name.get(block.name) == block:
67
+ continue
68
+ params = self._engine.eval_block(block)
69
+ if params is not None:
70
+ self._blocks_by_name[block.name] = block
64
71
  if self._on_eval is not None:
65
72
  self._on_eval(self._path, len(result.blocks))
66
73
 
@@ -71,7 +78,7 @@ def watch(
71
78
  debounce: float = 0.3,
72
79
  on_eval: Callable[[str, int], None] | None = None,
73
80
  ) -> None:
74
- """ファイルを監視し、変更のたびに全ループを再評価する。Ctrl+C で停止。
81
+ """ファイルを監視し、変更のたびに変更されたループを再評価する。Ctrl+C で停止。
75
82
 
76
83
  Args:
77
84
  path: 監視するファイルのパス
@@ -19,6 +19,10 @@ def _make_handler(
19
19
 
20
20
  _BASS_BLOCK = '@loop("bass")\ndef f(): pass\n'
21
21
  _BASS_PAD_BLOCK = '@loop("bass")\ndef bass(): pass\n@loop("pad")\ndef pad(): pass\n'
22
+ _BASS_PAD_CHANGED_BLOCK = (
23
+ '@loop("bass")\ndef bass(): pass\n'
24
+ '@loop("pad")\ndef pad(volume=0.5): pass\n'
25
+ )
22
26
  _BASS_MELODY_BLOCK = '@loop("bass")\ndef bass(): pass\n@loop("melody")\ndef melody(): pass\n'
23
27
 
24
28
 
@@ -69,6 +73,36 @@ def test_eval_all_loops_in_file(tmp_path: Path) -> None:
69
73
  assert engine.eval_block.call_count == 2
70
74
 
71
75
 
76
+ def test_eval_only_changed_loop_on_reload(tmp_path: Path) -> None:
77
+ target = tmp_path / "demo.py"
78
+ target.write_text(_BASS_PAD_BLOCK)
79
+ handler, engine = _make_handler(path=str(target), debounce=0.0)
80
+
81
+ handler.eval_now()
82
+ engine.eval_block.reset_mock()
83
+
84
+ target.write_text(_BASS_PAD_CHANGED_BLOCK)
85
+ handler.dispatch(str(target))
86
+ time.sleep(0.05)
87
+
88
+ assert engine.eval_block.call_count == 1
89
+ assert engine.eval_block.call_args.args[0].name == "pad"
90
+
91
+
92
+ def test_eval_skips_unchanged_loops_on_reload(tmp_path: Path) -> None:
93
+ target = tmp_path / "demo.py"
94
+ target.write_text(_BASS_PAD_BLOCK)
95
+ handler, engine = _make_handler(path=str(target), debounce=0.0)
96
+
97
+ handler.eval_now()
98
+ engine.eval_block.reset_mock()
99
+
100
+ handler.dispatch(str(target))
101
+ time.sleep(0.05)
102
+
103
+ engine.eval_block.assert_not_called()
104
+
105
+
72
106
  def test_eval_now_evaluates_without_file_event(tmp_path: Path) -> None:
73
107
  target = tmp_path / "demo.py"
74
108
  target.write_text(_BASS_BLOCK)
@@ -150,6 +184,7 @@ def test_adapter_on_moved_fires_eval(tmp_path: Path) -> None:
150
184
  adapter = _make_watchdog_adapter(str(target), engine)
151
185
  engine.reset_mock()
152
186
 
187
+ target.write_text('@loop("bass")\ndef bass(volume=0.5): pass\n')
153
188
  event = MagicMock()
154
189
  event.dest_path = str(target)
155
190
  adapter.on_moved(event)
@@ -166,6 +201,7 @@ def test_adapter_on_created_fires_eval(tmp_path: Path) -> None:
166
201
  adapter = _make_watchdog_adapter(str(target), engine)
167
202
  engine.reset_mock()
168
203
 
204
+ target.write_text('@loop("bass")\ndef bass(volume=0.5): pass\n')
169
205
  event = MagicMock()
170
206
  event.src_path = str(target)
171
207
  adapter.on_created(event)
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