Show More
@@ -0,0 +1,6 b'' | |||
|
1 | # Security Policy | |
|
2 | ||
|
3 | ## Reporting a Vulnerability | |
|
4 | ||
|
5 | All IPython and Jupyter security are handled via security@ipython.org. | |
|
6 | You can find more informations on the Jupyter website. https://jupyter.org/security |
@@ -2,6 +2,10 b' name: Run tests' | |||
|
2 | 2 | |
|
3 | 3 | on: |
|
4 | 4 | push: |
|
5 | branches: | |
|
6 | - main | |
|
7 | - master | |
|
8 | - '*.x' | |
|
5 | 9 | pull_request: |
|
6 | 10 | # Run weekly on Monday at 1:23 UTC |
|
7 | 11 | schedule: |
@@ -83,7 +83,7 b' def _display_mimetype(mimetype, objs, raw=False, metadata=None):' | |||
|
83 | 83 | if raw: |
|
84 | 84 | # turn list of pngdata into list of { 'image/png': pngdata } |
|
85 | 85 | objs = [ {mimetype: obj} for obj in objs ] |
|
86 | display(*objs, raw=raw, metadata=metadata, include=[mimetype]) | |
|
86 | display_functions.display(*objs, raw=raw, metadata=metadata, include=[mimetype]) | |
|
87 | 87 | |
|
88 | 88 | #----------------------------------------------------------------------------- |
|
89 | 89 | # Main functions |
@@ -517,10 +517,10 b' class ProgressBar(DisplayObject):' | |||
|
517 | 517 | self.html_width, self.total, self.progress) |
|
518 | 518 | |
|
519 | 519 | def display(self): |
|
520 | display(self, display_id=self._display_id) | |
|
520 | display_functions.display(self, display_id=self._display_id) | |
|
521 | 521 | |
|
522 | 522 | def update(self): |
|
523 | display(self, display_id=self._display_id, update=True) | |
|
523 | display_functions.display(self, display_id=self._display_id, update=True) | |
|
524 | 524 | |
|
525 | 525 | @property |
|
526 | 526 | def progress(self): |
@@ -694,7 +694,7 b' class GeoJSON(JSON):' | |||
|
694 | 694 | metadata = { |
|
695 | 695 | 'application/geo+json': self.metadata |
|
696 | 696 | } |
|
697 | display(bundle, metadata=metadata, raw=True) | |
|
697 | display_functions.display(bundle, metadata=metadata, raw=True) | |
|
698 | 698 | |
|
699 | 699 | class Javascript(TextDisplayObject): |
|
700 | 700 |
@@ -508,8 +508,11 b' def make_tokens_by_line(lines:List[str]):' | |||
|
508 | 508 | # reexported from token on 3.7+ |
|
509 | 509 | NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore |
|
510 | 510 | tokens_by_line:List[List[Any]] = [[]] |
|
511 |
if len(lines) > 1 and not lines[0].endswith(( |
|
|
512 | warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified") | |
|
511 | if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")): | |
|
512 | warnings.warn( | |
|
513 | "`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified", | |
|
514 | stacklevel=2, | |
|
515 | ) | |
|
513 | 516 | parenlev = 0 |
|
514 | 517 | try: |
|
515 | 518 | for token in tokenize.generate_tokens(iter(lines).__next__): |
@@ -782,9 +785,6 b' class MaybeAsyncCompile(Compile):' | |||
|
782 | 785 | super().__init__() |
|
783 | 786 | self.flags |= extra_flags |
|
784 | 787 | |
|
785 | def __call__(self, *args, **kwds): | |
|
786 | return compile(*args, **kwds) | |
|
787 | ||
|
788 | 788 | |
|
789 | 789 | class MaybeAsyncCommandCompiler(CommandCompiler): |
|
790 | 790 | def __init__(self, extra_flags=0): |
@@ -37,6 +37,38 b' arguments::' | |||
|
37 | 37 | -o OPTION, --option OPTION |
|
38 | 38 | An optional argument. |
|
39 | 39 | |
|
40 | Here is an elaborated example that uses default parameters in `argument` and calls the `args` in the cell magic:: | |
|
41 | ||
|
42 | from IPython.core.magic import register_cell_magic | |
|
43 | from IPython.core.magic_arguments import (argument, magic_arguments, | |
|
44 | parse_argstring) | |
|
45 | ||
|
46 | ||
|
47 | @magic_arguments() | |
|
48 | @argument( | |
|
49 | "--option", | |
|
50 | "-o", | |
|
51 | help=("Add an option here"), | |
|
52 | ) | |
|
53 | @argument( | |
|
54 | "--style", | |
|
55 | "-s", | |
|
56 | default="foo", | |
|
57 | help=("Add some style arguments"), | |
|
58 | ) | |
|
59 | @register_cell_magic | |
|
60 | def my_cell_magic(line, cell): | |
|
61 | args = parse_argstring(my_cell_magic, line) | |
|
62 | print(f"{args.option=}") | |
|
63 | print(f"{args.style=}") | |
|
64 | print(f"{cell=}") | |
|
65 | ||
|
66 | In a jupyter notebook, this cell magic can be executed like this:: | |
|
67 | ||
|
68 | %%my_cell_magic -o Hello | |
|
69 | print("bar") | |
|
70 | i = 42 | |
|
71 | ||
|
40 | 72 | Inheritance diagram: |
|
41 | 73 | |
|
42 | 74 | .. inheritance-diagram:: IPython.core.magic_arguments |
@@ -4,6 +4,7 b' Line-based transformers are the simpler ones; token-based transformers are' | |||
|
4 | 4 | more complex. See test_inputtransformer2_line for tests for line-based |
|
5 | 5 | transformations. |
|
6 | 6 | """ |
|
7 | import platform | |
|
7 | 8 | import string |
|
8 | 9 | import sys |
|
9 | 10 | from textwrap import dedent |
@@ -291,6 +292,7 b' def test_check_complete_param(code, expected, number):' | |||
|
291 | 292 | assert cc(code) == (expected, number) |
|
292 | 293 | |
|
293 | 294 | |
|
295 | @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy") | |
|
294 | 296 | @pytest.mark.xfail( |
|
295 | 297 | reason="Bug in python 3.9.8 – bpo 45738", |
|
296 | 298 | condition=sys.version_info in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)], |
@@ -319,7 +321,16 b' def test_check_complete():' | |||
|
319 | 321 | assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2) |
|
320 | 322 | |
|
321 | 323 | |
|
322 | def test_check_complete_II(): | |
|
324 | @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy") | |
|
325 | @pytest.mark.parametrize( | |
|
326 | "value, expected", | |
|
327 | [ | |
|
328 | ('''def foo():\n """''', ("incomplete", 4)), | |
|
329 | ("""async with example:\n pass""", ("incomplete", 4)), | |
|
330 | ("""async with example:\n pass\n """, ("complete", None)), | |
|
331 | ], | |
|
332 | ) | |
|
333 | def test_check_complete_II(value, expected): | |
|
323 | 334 | """ |
|
324 | 335 | Test that multiple line strings are properly handled. |
|
325 | 336 | |
@@ -327,25 +338,31 b' def test_check_complete_II():' | |||
|
327 | 338 | |
|
328 | 339 | """ |
|
329 | 340 | cc = ipt2.TransformerManager().check_complete |
|
330 | assert cc('''def foo():\n """''') == ("incomplete", 4) | |
|
331 | ||
|
332 | ||
|
333 | def test_check_complete_invalidates_sunken_brackets(): | |
|
341 | assert cc(value) == expected | |
|
342 | ||
|
343 | ||
|
344 | @pytest.mark.parametrize( | |
|
345 | "value, expected", | |
|
346 | [ | |
|
347 | (")", ("invalid", None)), | |
|
348 | ("]", ("invalid", None)), | |
|
349 | ("}", ("invalid", None)), | |
|
350 | (")(", ("invalid", None)), | |
|
351 | ("][", ("invalid", None)), | |
|
352 | ("}{", ("invalid", None)), | |
|
353 | ("]()(", ("invalid", None)), | |
|
354 | ("())(", ("invalid", None)), | |
|
355 | (")[](", ("invalid", None)), | |
|
356 | ("()](", ("invalid", None)), | |
|
357 | ], | |
|
358 | ) | |
|
359 | def test_check_complete_invalidates_sunken_brackets(value, expected): | |
|
334 | 360 | """ |
|
335 | 361 | Test that a single line with more closing brackets than the opening ones is |
|
336 | 362 | interpreted as invalid |
|
337 | 363 | """ |
|
338 | 364 | cc = ipt2.TransformerManager().check_complete |
|
339 | assert cc(")") == ("invalid", None) | |
|
340 | assert cc("]") == ("invalid", None) | |
|
341 | assert cc("}") == ("invalid", None) | |
|
342 | assert cc(")(") == ("invalid", None) | |
|
343 | assert cc("][") == ("invalid", None) | |
|
344 | assert cc("}{") == ("invalid", None) | |
|
345 | assert cc("]()(") == ("invalid", None) | |
|
346 | assert cc("())(") == ("invalid", None) | |
|
347 | assert cc(")[](") == ("invalid", None) | |
|
348 | assert cc("()](") == ("invalid", None) | |
|
365 | assert cc(value) == expected | |
|
349 | 366 | |
|
350 | 367 | |
|
351 | 368 | def test_null_cleanup_transformer(): |
@@ -406,13 +406,18 b' class TestShellGlob(unittest.TestCase):' | |||
|
406 | 406 | self.check_match(patterns, matches) |
|
407 | 407 | |
|
408 | 408 | |
|
409 | # TODO : pytest.mark.parametrise once nose is gone. | |
|
410 | def test_unescape_glob(): | |
|
411 | assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?" | |
|
412 | assert path.unescape_glob(r"\\*") == r"\*" | |
|
413 | assert path.unescape_glob(r"\\\*") == r"\*" | |
|
414 | assert path.unescape_glob(r"\\a") == r"\a" | |
|
415 | assert path.unescape_glob(r"\a") == r"\a" | |
|
409 | @pytest.mark.parametrize( | |
|
410 | "globstr, unescaped_globstr", | |
|
411 | [ | |
|
412 | (r"\*\[\!\]\?", "*[!]?"), | |
|
413 | (r"\\*", r"\*"), | |
|
414 | (r"\\\*", r"\*"), | |
|
415 | (r"\\a", r"\a"), | |
|
416 | (r"\a", r"\a"), | |
|
417 | ], | |
|
418 | ) | |
|
419 | def test_unescape_glob(globstr, unescaped_globstr): | |
|
420 | assert path.unescape_glob(globstr) == unescaped_globstr | |
|
416 | 421 | |
|
417 | 422 | |
|
418 | 423 | @onlyif_unicode_paths |
@@ -1,4 +1,3 b'' | |||
|
1 | # encoding: utf-8 | |
|
2 | 1 |
|
|
3 | 2 | Tests for platutils.py |
|
4 | 3 | """ |
@@ -56,34 +55,36 b' def test_find_cmd_fail():' | |||
|
56 | 55 | pytest.raises(FindCmdError, find_cmd, "asdfasdf") |
|
57 | 56 | |
|
58 | 57 | |
|
59 | # TODO: move to pytest.mark.parametrize once nose gone | |
|
60 | 58 | @dec.skip_win32 |
|
61 | def test_arg_split(): | |
|
62 | """Ensure that argument lines are correctly split like in a shell.""" | |
|
63 | tests = [['hi', ['hi']], | |
|
64 |
|
|
|
65 |
|
|
|
59 | @pytest.mark.parametrize( | |
|
60 | "argstr, argv", | |
|
61 | [ | |
|
62 | ("hi", ["hi"]), | |
|
63 | ("hello there", ["hello", "there"]), | |
|
66 | 64 |
|
|
67 | 65 |
|
|
68 | 66 |
|
|
69 |
|
|
|
70 |
|
|
|
71 | ] | |
|
72 | for argstr, argv in tests: | |
|
67 | ("h\u01cello", ["h\u01cello"]), | |
|
68 | ('something "with quotes"', ["something", '"with quotes"']), | |
|
69 | ], | |
|
70 | ) | |
|
71 | def test_arg_split(argstr, argv): | |
|
72 | """Ensure that argument lines are correctly split like in a shell.""" | |
|
73 | 73 |
|
|
74 | 74 | |
|
75 | 75 | |
|
76 | # TODO: move to pytest.mark.parametrize once nose gone | |
|
77 | 76 | @dec.skip_if_not_win32 |
|
78 | def test_arg_split_win32(): | |
|
77 | @pytest.mark.parametrize( | |
|
78 | "argstr,argv", | |
|
79 | [ | |
|
80 | ("hi", ["hi"]), | |
|
81 | ("hello there", ["hello", "there"]), | |
|
82 | ("h\u01cello", ["h\u01cello"]), | |
|
83 | ('something "with quotes"', ["something", "with quotes"]), | |
|
84 | ], | |
|
85 | ) | |
|
86 | def test_arg_split_win32(argstr, argv): | |
|
79 | 87 | """Ensure that argument lines are correctly split like in a shell.""" |
|
80 | tests = [['hi', ['hi']], | |
|
81 | [u'hi', [u'hi']], | |
|
82 | ['hello there', ['hello', 'there']], | |
|
83 | [u'h\u01cello', [u'h\u01cello']], | |
|
84 | ['something "with quotes"', ['something', 'with quotes']], | |
|
85 | ] | |
|
86 | for argstr, argv in tests: | |
|
87 | 88 |
|
|
88 | 89 | |
|
89 | 90 | |
@@ -98,7 +99,7 b' class SubProcessTestCase(tt.TempFileMixin):' | |||
|
98 | 99 | self.mktmp('\n'.join(lines)) |
|
99 | 100 | |
|
100 | 101 | def test_system(self): |
|
101 |
status = system(' |
|
|
102 | status = system(f'{python} "{self.fname}"') | |
|
102 | 103 | self.assertEqual(status, 0) |
|
103 | 104 | |
|
104 | 105 | def test_system_quotes(self): |
@@ -145,11 +146,11 b' class SubProcessTestCase(tt.TempFileMixin):' | |||
|
145 | 146 | |
|
146 | 147 | status = self.assert_interrupts(command) |
|
147 | 148 | self.assertNotEqual( |
|
148 |
status, 0, "The process wasn't interrupted. Status: |
|
|
149 | status, 0, f"The process wasn't interrupted. Status: {status}" | |
|
149 | 150 | ) |
|
150 | 151 | |
|
151 | 152 | def test_getoutput(self): |
|
152 |
out = getoutput(' |
|
|
153 | out = getoutput(f'{python} "{self.fname}"') | |
|
153 | 154 | # we can't rely on the order the line buffered streams are flushed |
|
154 | 155 | try: |
|
155 | 156 | self.assertEqual(out, 'on stderron stdout') |
@@ -169,7 +170,7 b' class SubProcessTestCase(tt.TempFileMixin):' | |||
|
169 | 170 | self.assertEqual(out.strip(), '1') |
|
170 | 171 | |
|
171 | 172 | def test_getoutput_error(self): |
|
172 |
out, err = getoutputerror(' |
|
|
173 | out, err = getoutputerror(f'{python} "{self.fname}"') | |
|
173 | 174 | self.assertEqual(out, 'on stdout') |
|
174 | 175 | self.assertEqual(err, 'on stderr') |
|
175 | 176 | |
@@ -179,7 +180,7 b' class SubProcessTestCase(tt.TempFileMixin):' | |||
|
179 | 180 | self.assertEqual(out, '') |
|
180 | 181 | self.assertEqual(err, '') |
|
181 | 182 | self.assertEqual(code, 1) |
|
182 |
out, err, code = get_output_error_code(' |
|
|
183 | out, err, code = get_output_error_code(f'{python} "{self.fname}"') | |
|
183 | 184 | self.assertEqual(out, 'on stdout') |
|
184 | 185 | self.assertEqual(err, 'on stderr') |
|
185 | 186 | self.assertEqual(code, 0) |
@@ -80,22 +80,20 b' def test_columnize_random():' | |||
|
80 | 80 | ) |
|
81 | 81 | |
|
82 | 82 | |
|
83 | # TODO: pytest mark.parametrize once nose removed. | |
|
84 | def test_columnize_medium(): | |
|
83 | @pytest.mark.parametrize("row_first", [True, False]) | |
|
84 | def test_columnize_medium(row_first): | |
|
85 | 85 | """Test with inputs than shouldn't be wider than 80""" |
|
86 | 86 | size = 40 |
|
87 | 87 | items = [l*size for l in 'abc'] |
|
88 | for row_first in [True, False]: | |
|
89 | 88 |
|
|
90 | 89 |
|
|
91 | 90 | |
|
92 | 91 | |
|
93 | # TODO: pytest mark.parametrize once nose removed. | |
|
94 | def test_columnize_long(): | |
|
92 | @pytest.mark.parametrize("row_first", [True, False]) | |
|
93 | def test_columnize_long(row_first): | |
|
95 | 94 | """Test columnize with inputs longer than the display window""" |
|
96 | 95 | size = 11 |
|
97 | 96 | items = [l*size for l in 'abc'] |
|
98 | for row_first in [True, False]: | |
|
99 | 97 |
|
|
100 | 98 |
|
|
101 | 99 |
@@ -14,6 +14,7 b' recursive-exclude tools *' | |||
|
14 | 14 | exclude tools |
|
15 | 15 | exclude CONTRIBUTING.md |
|
16 | 16 | exclude .editorconfig |
|
17 | exclude SECURITY.md | |
|
17 | 18 | |
|
18 | 19 | graft scripts |
|
19 | 20 |
@@ -81,8 +81,8 b' profile with:' | |||
|
81 | 81 | $ ipython locate profile foo |
|
82 | 82 | /home/you/.ipython/profile_foo |
|
83 | 83 | |
|
84 |
These map to the utility functions: :func:`IPython. |
|
|
85 |
and :func:`IPython. |
|
|
84 | These map to the utility functions: :func:`IPython.paths.get_ipython_dir` | |
|
85 | and :func:`IPython.paths.locate_profile` respectively. | |
|
86 | 86 | |
|
87 | 87 | |
|
88 | 88 | .. _profiles_dev: |
@@ -48,7 +48,7 b' or have been turned into explicit errors for better error messages.' | |||
|
48 | 48 | I will use this occasion to add the following requests to anyone emitting a |
|
49 | 49 | deprecation warning: |
|
50 | 50 | |
|
51 |
- Please |
|
|
51 | - Please add at least ``stacklevel=2`` so that the warning is emitted into the | |
|
52 | 52 | caller context, and not the callee one. |
|
53 | 53 | - Please add **since which version** something is deprecated. |
|
54 | 54 |
@@ -15,6 +15,7 b' keywords = Interactive, Interpreter, Shell, Embedding' | |||
|
15 | 15 | platforms = Linux, Mac OSX, Windows |
|
16 | 16 | classifiers = |
|
17 | 17 | Framework :: IPython |
|
18 | Framework :: Jupyter | |
|
18 | 19 | Intended Audience :: Developers |
|
19 | 20 | Intended Audience :: Science/Research |
|
20 | 21 | License :: OSI Approved :: BSD License |
@@ -23,26 +24,69 b' classifiers =' | |||
|
23 | 24 | Programming Language :: Python :: 3 :: Only |
|
24 | 25 | Topic :: System :: Shells |
|
25 | 26 | |
|
26 | ||
|
27 | 27 | [options] |
|
28 | 28 | packages = find: |
|
29 | 29 | python_requires = >=3.8 |
|
30 | 30 | zip_safe = False |
|
31 | 31 | install_requires = |
|
32 | setuptools>=18.5 | |
|
33 | jedi>=0.16 | |
|
34 | black | |
|
32 | appnope; sys_platform == "darwin" | |
|
33 | backcall | |
|
34 | colorama; sys_platform == "win32" | |
|
35 | 35 | decorator |
|
36 | jedi>=0.16 | |
|
37 | matplotlib-inline | |
|
38 | pexpect>4.3; sys_platform != "win32" | |
|
36 | 39 | pickleshare |
|
37 | traitlets>=5 | |
|
38 | 40 | prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1 |
|
39 | 41 | pygments>=2.4.0 |
|
40 | backcall | |
|
42 | setuptools>=18.5 | |
|
41 | 43 | stack_data |
|
42 | matplotlib-inline | |
|
43 | pexpect>4.3; sys_platform != "win32" | |
|
44 | appnope; sys_platform == "darwin" | |
|
45 | colorama; sys_platform == "win32" | |
|
44 | traitlets>=5 | |
|
45 | ||
|
46 | [options.extras_require] | |
|
47 | black = | |
|
48 | black | |
|
49 | doc = | |
|
50 | Sphinx>=1.3 | |
|
51 | kernel = | |
|
52 | ipykernel | |
|
53 | nbconvert = | |
|
54 | nbconvert | |
|
55 | nbformat = | |
|
56 | nbformat | |
|
57 | notebook = | |
|
58 | ipywidgets | |
|
59 | notebook | |
|
60 | parallel = | |
|
61 | ipyparallel | |
|
62 | qtconsole = | |
|
63 | qtconsole | |
|
64 | terminal = | |
|
65 | test = | |
|
66 | pytest | |
|
67 | pytest-asyncio | |
|
68 | testpath | |
|
69 | test_extra = | |
|
70 | curio | |
|
71 | matplotlib!=3.2.0 | |
|
72 | nbformat | |
|
73 | numpy>=1.19 | |
|
74 | pandas | |
|
75 | pytest | |
|
76 | testpath | |
|
77 | trio | |
|
78 | all = | |
|
79 | %(black)s | |
|
80 | %(doc)s | |
|
81 | %(kernel)s | |
|
82 | %(nbconvert)s | |
|
83 | %(nbformat)s | |
|
84 | %(notebook)s | |
|
85 | %(parallel)s | |
|
86 | %(qtconsole)s | |
|
87 | %(terminal)s | |
|
88 | %(test_extra)s | |
|
89 | %(test)s | |
|
46 | 90 | |
|
47 | 91 | [options.packages.find] |
|
48 | 92 | exclude = |
@@ -137,46 +137,6 b" setup_args['cmdclass'] = {" | |||
|
137 | 137 | 'unsymlink': unsymlink, |
|
138 | 138 | } |
|
139 | 139 | |
|
140 | ||
|
141 | #--------------------------------------------------------------------------- | |
|
142 | # Handle scripts, dependencies, and setuptools specific things | |
|
143 | #--------------------------------------------------------------------------- | |
|
144 | ||
|
145 | # setuptools requirements | |
|
146 | ||
|
147 | extras_require = dict( | |
|
148 | parallel=["ipyparallel"], | |
|
149 | qtconsole=["qtconsole"], | |
|
150 | doc=["Sphinx>=1.3"], | |
|
151 | test=[ | |
|
152 | "pytest", | |
|
153 | "pytest-asyncio", | |
|
154 | "testpath", | |
|
155 | "pygments>=2.4.0", | |
|
156 | ], | |
|
157 | test_extra=[ | |
|
158 | "pytest", | |
|
159 | "testpath", | |
|
160 | "curio", | |
|
161 | "matplotlib!=3.2.0", | |
|
162 | "nbformat", | |
|
163 | "numpy>=1.19", | |
|
164 | "pandas", | |
|
165 | "pygments>=2.4.0", | |
|
166 | "trio", | |
|
167 | ], | |
|
168 | terminal=[], | |
|
169 | kernel=["ipykernel"], | |
|
170 | nbformat=["nbformat"], | |
|
171 | notebook=["notebook", "ipywidgets"], | |
|
172 | nbconvert=["nbconvert"], | |
|
173 | ) | |
|
174 | ||
|
175 | everything = set(chain.from_iterable(extras_require.values())) | |
|
176 | extras_require['all'] = list(sorted(everything)) | |
|
177 | ||
|
178 | setup_args["extras_require"] = extras_require | |
|
179 | ||
|
180 | 140 | #--------------------------------------------------------------------------- |
|
181 | 141 | # Do the actual setup now |
|
182 | 142 | #--------------------------------------------------------------------------- |
General Comments 0
You need to be logged in to leave comments.
Login now