rbx.cp 0.13.4__py3-none-any.whl → 0.13.5__py3-none-any.whl

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.
rbx/grading/steps.py CHANGED
@@ -1,15 +1,12 @@
1
1
  import asyncio
2
- import contextlib
3
2
  import dataclasses
4
3
  import functools
5
- import os
6
4
  import pathlib
7
5
  import re
8
6
  import shlex
9
7
  import shutil
10
8
  import subprocess
11
9
  import sys
12
- import tempfile
13
10
  from enum import Enum
14
11
  from typing import IO, Any, Dict, Iterable, List, Optional, Tuple, Union
15
12
 
@@ -20,9 +17,9 @@ from rich.text import Text
20
17
  from rbx import utils
21
18
  from rbx.config import get_bits_stdcpp, get_jngen, get_testlib
22
19
  from rbx.console import console
23
- from rbx.grading import grading_context, processing_context
20
+ from rbx.grading import grading_context
24
21
  from rbx.grading.judge.cacher import FileCacher
25
- from rbx.grading.judge.sandbox import SandboxBase, SandboxParams
22
+ from rbx.grading.judge.sandbox import SandboxBase, SandboxLog, SandboxParams
26
23
  from rbx.grading.judge.storage import copyfileobj
27
24
  from rbx.grading.limits import Limits
28
25
 
@@ -81,6 +78,7 @@ class DigestHolder(BaseModel):
81
78
 
82
79
  class GradingLogsHolder(BaseModel):
83
80
  run: Optional['RunLog'] = None
81
+ interactor_run: Optional['RunLog'] = None
84
82
  preprocess: Optional[List['PreprocessLog']] = None
85
83
  cached: bool = False
86
84
 
@@ -238,6 +236,7 @@ class RunLog(BaseModel):
238
236
  sandbox: str = ''
239
237
  warnings: bool = False
240
238
  metadata: Optional[RunLogMetadata] = None
239
+ exitindex: int = 0
241
240
 
242
241
  def get_run_language(self) -> Optional[str]:
243
242
  if self.metadata is None:
@@ -394,16 +393,18 @@ def _expand_part(part: str, sandbox: SandboxBase) -> List[str]:
394
393
  return [part]
395
394
 
396
395
 
397
- def _get_java_memory_limits(sandbox: SandboxBase) -> Tuple[int, int]:
398
- max_memory = sandbox.params.address_space
396
+ def _get_java_memory_limits(params: SandboxParams) -> Tuple[int, int]:
397
+ max_memory = params.address_space
399
398
  if max_memory is None:
400
399
  max_memory = 2048
401
400
  return max_memory, min(512, int(max_memory * 0.9))
402
401
 
403
402
 
404
- def _split_and_expand(command: str, sandbox: SandboxBase) -> List[str]:
403
+ def _split_and_expand(
404
+ command: str, sandbox: SandboxBase, params: SandboxParams
405
+ ) -> List[str]:
405
406
  res = []
406
- max_mem, init_mem = _get_java_memory_limits(sandbox)
407
+ max_mem, init_mem = _get_java_memory_limits(params)
407
408
  parts = shlex.split(command.format(memory=max_mem, initialMemory=init_mem))
408
409
  for part in parts:
409
410
  res.extend(_expand_part(part, sandbox))
@@ -640,6 +641,36 @@ def _check_for_compilation_warnings(
640
641
  )
641
642
 
642
643
 
644
+ def _build_run_log(
645
+ sandbox_log: SandboxLog,
646
+ sandbox: SandboxBase,
647
+ params: SandboxParams,
648
+ metadata: Optional[RunLogMetadata] = None,
649
+ ) -> RunLog:
650
+ execution_time = sandbox_log.execution_time
651
+ if execution_time is not None and (
652
+ sandbox_log.exitstatus == SandboxBase.EXIT_TIMEOUT
653
+ or sandbox_log.exitstatus == SandboxBase.EXIT_TIMEOUT_WALL
654
+ ):
655
+ execution_time = max(execution_time, (params.timeout or 0.0) / 1000)
656
+
657
+ run_log = RunLog(
658
+ exitcode=sandbox_log.exitcode,
659
+ exitstatus=sandbox_log.exitstatus,
660
+ time=execution_time,
661
+ memory=sandbox_log.memory_used,
662
+ metadata=metadata,
663
+ sandbox=sandbox_log.dump_other_logs(),
664
+ exitindex=sandbox_log.exit_index,
665
+ )
666
+ if metadata is not None and metadata.is_sanitized:
667
+ run_log.warnings = _check_for_sanitizer_warnings(
668
+ sandbox,
669
+ params.stderr_file,
670
+ )
671
+ return run_log
672
+
673
+
643
674
  def compile(
644
675
  commands: List[str],
645
676
  params: SandboxParams,
@@ -656,29 +687,20 @@ def compile(
656
687
  return True
657
688
 
658
689
  logs: List[PreprocessLog] = []
659
- sandbox.set_params(
660
- params.model_copy(deep=True)
661
- ) # Copy to allow further modification.
690
+ params = params.model_copy(deep=True) # Copy to allow further modification.
662
691
 
663
692
  for i, command in enumerate(commands):
664
693
  _maybe_complain_about_sanitization(command)
665
- cmd = _split_and_expand(command, sandbox)
694
+ cmd = _split_and_expand(command, sandbox, params)
666
695
  stdout_file = pathlib.PosixPath(f'compile-{i}.stdout')
667
696
  stderr_file = pathlib.PosixPath(f'compile-{i}.stderr')
668
- sandbox.params.set_stdall(stdout=stdout_file, stderr=stderr_file)
697
+ params.set_stdall(stdout=stdout_file, stderr=stderr_file)
669
698
 
670
699
  # Remove memory constraints for Java.
671
700
  if is_java_like_command(get_exe_from_command(command)):
672
- sandbox.params.address_space = None
701
+ params.address_space = None
673
702
 
674
- if not sandbox.execute_without_std(cmd):
675
- console.print(
676
- '[error]Sandbox crashed while processing command:[/error]',
677
- utils.highlight_json_obj(cmd),
678
- '[error]and logs[/error]',
679
- sandbox.debug_message(),
680
- )
681
- return False
703
+ sandbox_log = sandbox.run(cmd, params)
682
704
 
683
705
  std_outputs = [
684
706
  sandbox.get_file_to_string(stderr_file, maxlen=None)
@@ -691,13 +713,13 @@ def compile(
691
713
 
692
714
  log = PreprocessLog(
693
715
  cmd=cmd,
694
- exitcode=sandbox.get_exit_code(),
695
- exitstatus=sandbox.get_exit_status(),
696
- time=sandbox.get_execution_time(),
697
- memory=sandbox.get_memory_used(),
716
+ exitcode=sandbox_log.exitcode,
717
+ exitstatus=sandbox_log.exitstatus,
718
+ time=sandbox_log.execution_time,
719
+ memory=sandbox_log.memory_used,
698
720
  warnings=_check_for_compilation_warnings(sandbox, stderr_file),
699
721
  log='\n'.join(std_outputs),
700
- sandbox=sandbox.get_detailed_logs(),
722
+ sandbox=sandbox_log.dump_other_logs(),
701
723
  )
702
724
  logs.append(log)
703
725
 
@@ -730,48 +752,19 @@ async def run(
730
752
 
731
753
  _process_input_artifacts(artifacts, sandbox)
732
754
  _process_fifos(artifacts, sandbox)
733
- cmd = _split_and_expand(command, sandbox)
734
- sandbox.set_params(
735
- params.model_copy(deep=True)
736
- ) # Copy to allow further modification.
755
+ cmd = _split_and_expand(command, sandbox, params)
756
+ params = params.model_copy(deep=True) # Copy to allow further modification.
737
757
 
738
758
  # Remove memory constraints for Java.
739
759
  if is_java_like_command(get_exe_from_command(command)):
740
- sandbox.params.address_space = None
760
+ params.address_space = None
741
761
 
742
- sandbox.pid_event.set_loop(asyncio.get_event_loop())
743
- if not await asyncio.to_thread(sandbox.execute_without_std, cmd):
744
- console.print(
745
- '[error]Sandbox crashed while processing command:[/error]',
746
- utils.highlight_json_obj(cmd),
747
- '[error]and logs[/error]',
748
- sandbox.debug_message(),
749
- )
750
- return None
762
+ sandbox_log = await asyncio.to_thread(sandbox.run, cmd, params)
751
763
 
752
764
  if not _process_output_artifacts(artifacts, sandbox):
753
765
  return None
754
766
 
755
- execution_time = sandbox.get_execution_time()
756
- if execution_time is not None and (
757
- sandbox.get_exit_status() == SandboxBase.EXIT_TIMEOUT
758
- or sandbox.get_exit_status() == SandboxBase.EXIT_TIMEOUT_WALL
759
- ):
760
- execution_time = max(execution_time, (params.timeout or 0.0) / 1000)
761
-
762
- run_log = RunLog(
763
- exitcode=sandbox.get_exit_code(),
764
- exitstatus=sandbox.get_exit_status(),
765
- time=sandbox.get_execution_time(),
766
- memory=sandbox.get_memory_used(),
767
- metadata=metadata,
768
- sandbox=sandbox.get_detailed_logs(),
769
- )
770
- if metadata is not None and metadata.is_sanitized:
771
- run_log.warnings = _check_for_sanitizer_warnings(
772
- sandbox,
773
- params.stderr_file,
774
- )
767
+ run_log = _build_run_log(sandbox_log, sandbox, params, metadata)
775
768
  if artifacts.logs is not None:
776
769
  artifacts.logs.run = run_log.model_copy()
777
770
  return run_log
@@ -781,45 +774,53 @@ async def run(
781
774
  class CoordinatedRunParams:
782
775
  command: str
783
776
  params: SandboxParams
784
- sandbox: SandboxBase
785
- artifacts: GradingArtifacts
786
777
  metadata: Optional[RunLogMetadata] = None
787
778
 
788
779
 
789
780
  async def run_coordinated(
790
781
  interactor: CoordinatedRunParams,
791
782
  solution: CoordinatedRunParams,
783
+ artifacts: GradingArtifacts,
784
+ sandbox: SandboxBase,
785
+ merged_capture: Optional[pathlib.Path] = None,
792
786
  ) -> Tuple[Optional[RunLog], Optional[RunLog]]:
793
- def run_one(params: CoordinatedRunParams) -> asyncio.Task[Optional[RunLog]]:
794
- return asyncio.create_task(
795
- run(
796
- params.command,
797
- params.params,
798
- params.sandbox,
799
- params.artifacts,
800
- params.metadata,
801
- )
802
- )
787
+ sandbox.reset()
803
788
 
804
- # Use interactor PID as the process group id.
805
- interactor_task = run_one(interactor)
806
- solution.sandbox.params.pgid = await interactor.sandbox.get_pid()
807
- solution_task = run_one(solution)
789
+ _process_input_artifacts(artifacts, sandbox)
790
+ _process_fifos(artifacts, sandbox)
808
791
 
809
- await processing_context.wait_all([interactor.sandbox, solution.sandbox])
792
+ interactor_cmd = _split_and_expand(interactor.command, sandbox, interactor.params)
793
+ solution_cmd = _split_and_expand(solution.command, sandbox, solution.params)
810
794
 
811
- return await asyncio.gather(interactor_task, solution_task)
795
+ interactor_params = interactor.params.model_copy(deep=True)
796
+ solution_params = solution.params.model_copy(deep=True)
812
797
 
798
+ if is_java_like_command(get_exe_from_command(solution.command)):
799
+ solution_params.address_space = None
800
+
801
+ solution_sandbox_log, interactor_sandbox_log = sandbox.run_communication(
802
+ solution_cmd,
803
+ solution_params,
804
+ interactor_cmd,
805
+ interactor_params,
806
+ merged_capture,
807
+ )
813
808
 
814
- def _normalize_checked_words(s: str) -> Tuple[str, ...]:
815
- return tuple(s.split())
809
+ if not _process_output_artifacts(artifacts, sandbox):
810
+ return None, None
816
811
 
812
+ solution_log = _build_run_log(
813
+ solution_sandbox_log, sandbox, solution.params, solution.metadata
814
+ )
815
+ interactor_log = _build_run_log(
816
+ interactor_sandbox_log, sandbox, interactor.params, interactor.metadata
817
+ )
817
818
 
818
- def _wcmp_check(expected: str, output: str) -> Outcome:
819
- if _normalize_checked_words(expected) == _normalize_checked_words(output):
820
- return Outcome.ACCEPTED
819
+ if artifacts.logs is not None:
820
+ artifacts.logs.run = solution_log
821
+ artifacts.logs.interactor_run = interactor_log
821
822
 
822
- return Outcome.WRONG_ANSWER
823
+ return solution_log, interactor_log
823
824
 
824
825
 
825
826
  def get_checker_sandbox_params() -> SandboxParams:
@@ -830,116 +831,3 @@ def get_checker_sandbox_params() -> SandboxParams:
830
831
  params.add_mapped_directory(pathlib.Path('/usr'))
831
832
  params.add_mapped_directory(pathlib.Path('/etc'))
832
833
  return params
833
-
834
-
835
- def _check(
836
- sandbox: SandboxBase,
837
- testcase: TestcaseIO,
838
- output_path: pathlib.Path,
839
- should_use_python_checker: bool = False,
840
- ) -> CheckerResult:
841
- if testcase.output is None:
842
- # No output to compare.
843
- return CheckerResult(outcome=Outcome.ACCEPTED)
844
-
845
- if should_use_python_checker:
846
- # Use default wcmp checker.
847
- expected = testcase.output.read_text()
848
- output = output_path.read_text()
849
-
850
- return CheckerResult(outcome=_wcmp_check(expected, output))
851
-
852
- sandbox.params.set_stdall(
853
- stdin=None,
854
- stdout=pathlib.PosixPath('stdout.txt'),
855
- stderr=pathlib.PosixPath('stderr.txt'),
856
- )
857
-
858
- sandbox.create_file_from_string(
859
- pathlib.PosixPath('expected.txt'), testcase.output.read_text(), override=True
860
- )
861
- sandbox.create_file_from_string(
862
- pathlib.PosixPath('output.txt'), output_path.read_text(), override=True
863
- )
864
- sandbox.create_file_from_string(
865
- pathlib.PosixPath('input.txt'),
866
- testcase.input.read_text() if testcase.input is not None else '',
867
- override=True,
868
- )
869
-
870
- if not sandbox.execute_without_std(
871
- ['./checker', 'input.txt', 'output.txt', 'expected.txt'],
872
- ):
873
- console.print(
874
- '[error]Sandbox crashed while running checker.[/error]',
875
- '[error]and logs[/error]',
876
- sandbox.debug_message(),
877
- )
878
- return CheckerResult(outcome=Outcome.INTERNAL_ERROR)
879
-
880
- checker_stderr = sandbox.get_file_to_string(
881
- pathlib.PosixPath('stderr.txt'), maxlen=None
882
- )
883
- if sandbox.get_exit_code() in [1, 2]:
884
- return CheckerResult(outcome=Outcome.WRONG_ANSWER, message=checker_stderr)
885
- if sandbox.get_exit_code() == 3:
886
- return CheckerResult(outcome=Outcome.JUDGE_FAILED, message=checker_stderr)
887
- return CheckerResult(outcome=Outcome.ACCEPTED, message=checker_stderr)
888
-
889
-
890
- # Always assume a `checker` executable in the sandbox if should use checker.
891
- def evaluate(
892
- sandbox: SandboxBase,
893
- testcase: TestcaseIO,
894
- log: Optional[TestcaseLog],
895
- artifacts: GradingArtifacts,
896
- should_use_python_checker: bool = False,
897
- ) -> Evaluation:
898
- if log is None:
899
- return Evaluation(
900
- testcase=testcase,
901
- log=TestcaseLog(),
902
- result=CheckerResult(outcome=Outcome.INTERNAL_ERROR),
903
- )
904
- if log.exitstatus != SandboxBase.EXIT_OK:
905
- return Evaluation(
906
- testcase=testcase,
907
- log=log,
908
- result=CheckerResult(outcome=Outcome.RUNTIME_ERROR),
909
- )
910
-
911
- if not testcase.output:
912
- # No output to compare.
913
- return Evaluation(
914
- testcase=testcase, log=log, result=CheckerResult(outcome=Outcome.ACCEPTED)
915
- )
916
-
917
- _process_input_artifacts(artifacts, sandbox)
918
- if log.stdout_absolute_path is None:
919
- return Evaluation(
920
- testcase=testcase,
921
- log=log,
922
- result=CheckerResult(outcome=Outcome.INTERNAL_ERROR, message='No output'),
923
- )
924
-
925
- checker_result = _check(
926
- sandbox,
927
- testcase,
928
- log.stdout_absolute_path,
929
- should_use_python_checker=should_use_python_checker,
930
- )
931
- return Evaluation(
932
- testcase=testcase,
933
- log=log,
934
- result=checker_result,
935
- )
936
-
937
-
938
- @contextlib.contextmanager
939
- def make_fifos():
940
- with tempfile.TemporaryDirectory() as temp_dir:
941
- fifo_in = pathlib.PosixPath(temp_dir) / 'fifo.in'
942
- fifo_out = pathlib.PosixPath(temp_dir) / 'fifo.out'
943
- os.mkfifo(fifo_in)
944
- os.mkfifo(fifo_out)
945
- yield fifo_in, fifo_out
@@ -1,3 +1,4 @@
1
+ import pathlib
1
2
  from typing import Any, Dict, List, Optional, Tuple
2
3
 
3
4
  from rbx.grading import grading_context, profiling, steps
@@ -88,10 +89,12 @@ async def run(
88
89
  async def run_coordinated(
89
90
  interactor: steps.CoordinatedRunParams,
90
91
  solution: steps.CoordinatedRunParams,
92
+ artifacts: GradingArtifacts,
93
+ sandbox: SandboxBase,
91
94
  dependency_cache: DependencyCache,
95
+ merged_capture: Optional[pathlib.Path] = None,
92
96
  ) -> Tuple[Optional[RunLog], Optional[RunLog]]:
93
- interactor.artifacts.logs = GradingLogsHolder()
94
- solution.artifacts.logs = GradingLogsHolder()
97
+ artifacts.logs = GradingLogsHolder()
95
98
 
96
99
  cacheable_params = {
97
100
  **_get_prefixed_cacheable_params(
@@ -114,18 +117,24 @@ async def run_coordinated(
114
117
  cached_profile = profiling.Profiler('steps.run_coordinated[cached]', start=True)
115
118
  with dependency_cache(
116
119
  [interactor.command, solution.command],
117
- [interactor.artifacts, solution.artifacts],
120
+ [artifacts],
118
121
  cacheable_params,
119
122
  ) as is_cached:
120
123
  if not is_cached:
121
124
  with profiling.Profiler('steps.run_coordinated'):
122
125
  profiling.add_to_counter('steps.run_coordinated')
123
- await steps.run_coordinated(interactor, solution)
126
+ await steps.run_coordinated(
127
+ interactor,
128
+ solution,
129
+ artifacts,
130
+ sandbox,
131
+ merged_capture=merged_capture,
132
+ )
124
133
  else:
125
134
  cached_profile.stop()
126
135
  profiling.add_to_counter('steps.run_coordinated[cached]')
127
136
 
128
137
  return (
129
- interactor.artifacts.logs.run,
130
- solution.artifacts.logs.run,
138
+ artifacts.logs.interactor_run,
139
+ artifacts.logs.run,
131
140
  )
@@ -19,8 +19,6 @@ solutions:
19
19
  outcome: "ACCEPTED"
20
20
  - path: "sols/wa.cpp"
21
21
  outcome: "WRONG_ANSWER"
22
- - path: "sols/slow.cpp"
23
- outcome: "TLE_OR_RTE" # Can be TLE too
24
22
  statements:
25
23
  - name: "statement-en"
26
24
  title: "New Problem"
@@ -1,5 +1,7 @@
1
1
  #ifndef _RBX_H
2
2
  #define _RBX_H
3
+ #include <cstdint>
4
+ #include <limits>
3
5
  #include <optional>
4
6
  #include <stdexcept>
5
7
  #include <string>
@@ -9,7 +11,7 @@ std::optional<std::string> getStringVar(std::string name) {
9
11
  return std::nullopt;
10
12
  }
11
13
 
12
- std::optional<int> getIntVar(std::string name) {
14
+ std::optional<int64_t> getIntVar(std::string name) {
13
15
  //<rbx::int_var>
14
16
  return std::nullopt;
15
17
  }
@@ -26,7 +28,46 @@ std::optional<bool> getBoolVar(std::string name) {
26
28
 
27
29
  template <typename T> T getVar(std::string name);
28
30
 
29
- template <> int getVar<int>(std::string name) {
31
+ template <> int32_t getVar<int32_t>(std::string name) {
32
+ auto opt = getIntVar(name);
33
+ if (!opt.has_value()) {
34
+ throw std::runtime_error("Variable " + name +
35
+ " is not an integer or could not be found");
36
+ }
37
+ if (opt.value() < std::numeric_limits<int32_t>::min() ||
38
+ opt.value() > std::numeric_limits<int32_t>::max()) {
39
+ throw std::runtime_error("Variable " + name + " of value " +
40
+ std::to_string(opt.value()) +
41
+ " does not fit in int32_t");
42
+ }
43
+ return opt.value();
44
+ }
45
+
46
+ template <> uint32_t getVar<uint32_t>(std::string name) {
47
+ auto opt = getIntVar(name);
48
+ if (!opt.has_value()) {
49
+ throw std::runtime_error("Variable " + name +
50
+ " is not an integer or could not be found");
51
+ }
52
+ if (opt.value() < std::numeric_limits<uint32_t>::min() ||
53
+ opt.value() > std::numeric_limits<uint32_t>::max()) {
54
+ throw std::runtime_error("Variable " + name + " of value " +
55
+ std::to_string(opt.value()) +
56
+ " does not fit in uint32_t");
57
+ }
58
+ return opt.value();
59
+ }
60
+
61
+ template <> int64_t getVar<int64_t>(std::string name) {
62
+ auto opt = getIntVar(name);
63
+ if (!opt.has_value()) {
64
+ throw std::runtime_error("Variable " + name +
65
+ " is not an integer or could not be found");
66
+ }
67
+ return opt.value();
68
+ }
69
+
70
+ template <> uint64_t getVar<uint64_t>(std::string name) {
30
71
  auto opt = getIntVar(name);
31
72
  if (!opt.has_value()) {
32
73
  throw std::runtime_error("Variable " + name +
rbx/testing_utils.py CHANGED
@@ -16,6 +16,13 @@ def get_testdata_path() -> pathlib.Path:
16
16
  return file.parent
17
17
 
18
18
 
19
+ def get_resources_path() -> pathlib.Path:
20
+ with importlib.resources.as_file(
21
+ importlib.resources.files('rbx') / 'resources' / 'default_setter_config.yml'
22
+ ) as file:
23
+ return file.parent
24
+
25
+
19
26
  def clear_all_functools_cache():
20
27
  from rbx.box import environment, header, package
21
28
 
rbx/utils.py CHANGED
@@ -5,12 +5,15 @@ import json
5
5
  import os
6
6
  import os.path
7
7
  import pathlib
8
+ import re
8
9
  import resource
10
+ import shutil
9
11
  import subprocess
10
12
  import sys
11
- from typing import Any, Optional, Type, TypeVar
13
+ from typing import Any, Optional, Type, TypeVar, Union
12
14
 
13
15
  import rich
16
+ import rich.markup
14
17
  import rich.prompt
15
18
  import rich.status
16
19
  import ruyaml
@@ -24,6 +27,7 @@ from rbx.console import console
24
27
 
25
28
  T = TypeVar('T', bound=BaseModel)
26
29
  APP_NAME = 'rbx'
30
+ PathOrStr = Union[pathlib.Path, str]
27
31
 
28
32
 
29
33
  def create_and_write(path: pathlib.Path, *args, **kwargs):
@@ -37,6 +41,10 @@ def highlight_str(s: str) -> text.Text:
37
41
  return txt
38
42
 
39
43
 
44
+ def escape_markup(s: str) -> str:
45
+ return rich.markup.escape(s, _escape=re.compile(r'(\\*)(\[)').sub)
46
+
47
+
40
48
  def abspath(path: pathlib.Path) -> pathlib.Path:
41
49
  return pathlib.Path(os.path.abspath(path))
42
50
 
@@ -185,6 +193,48 @@ def new_cd(x: pathlib.Path):
185
193
  os.chdir(d)
186
194
 
187
195
 
196
+ def _safe_match(matcher, path):
197
+ try:
198
+ return matcher(path)
199
+ except ValueError:
200
+ return False
201
+
202
+
203
+ def copytree_honoring_gitignore(
204
+ src: pathlib.Path, dst: pathlib.Path, extra_gitignore: Optional[str] = None
205
+ ):
206
+ from gitignore_parser import parse_gitignore, parse_gitignore_str
207
+
208
+ ignore_matchers = []
209
+
210
+ if extra_gitignore is not None:
211
+ ignore_matchers.append(parse_gitignore_str(extra_gitignore, base_dir=src))
212
+
213
+ for file in src.rglob('.gitignore'):
214
+ if file.is_file():
215
+ ignore_matchers.append(parse_gitignore(file))
216
+
217
+ # TODO: use recursive walk
218
+ for file in src.rglob('*'):
219
+ matching_file = file
220
+ ignored = False
221
+ while matching_file.is_relative_to(src):
222
+ if any(
223
+ _safe_match(ignore_matcher, matching_file)
224
+ for ignore_matcher in ignore_matchers
225
+ ):
226
+ ignored = True
227
+ break
228
+ matching_file = matching_file.parent
229
+ if ignored:
230
+ continue
231
+ rel = relpath(file, src)
232
+ if file.is_file():
233
+ write_to = dst / rel
234
+ write_to.parent.mkdir(parents=True, exist_ok=True)
235
+ shutil.copyfile(file, write_to)
236
+
237
+
188
238
  class StatusProgress(rich.status.Status):
189
239
  _message: str
190
240
  processed: int
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: rbx.cp
3
- Version: 0.13.4
3
+ Version: 0.13.5
4
4
  Summary:
5
5
  Author: Roberto Sales
6
6
  Requires-Python: >=3.9.1,<4.0.0