mirror of
https://github.com/gryf/fs-uae-wrapper.git
synced 2026-02-02 14:15:45 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b0ef15eae | |||
| 59bd1b6029 | |||
| f5e6471555 | |||
| 4c61c3d7ea | |||
| 7e3d68624f | |||
| f311605019 | |||
| 418e480fb5 | |||
| 118d758ec7 | |||
| 3b597e34ee | |||
| 7b40974779 | |||
| b4ab8ac4f5 | |||
| 8d8d38d5c0 | |||
| bd0aa3dee4 | |||
| d2a9f39fd9 |
33
.github/workflows/test.yml
vendored
Normal file
33
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# This workflow will install Python dependencies, run tests and lint with a single version of Python
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
|
||||||
|
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python 3.10
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install flake8 pytest tox coverage pytest-cov pytest-pep8
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
tox
|
||||||
11
.travis.yml
11
.travis.yml
@@ -1,11 +0,0 @@
|
|||||||
language: python
|
|
||||||
env:
|
|
||||||
- TOXENV=py27
|
|
||||||
- TOXENV=py27-flake8
|
|
||||||
- TOXENV=py3
|
|
||||||
- TOXENV=py3-flake8
|
|
||||||
install: pip install tox
|
|
||||||
script: tox
|
|
||||||
before_install:
|
|
||||||
- sudo apt-get update
|
|
||||||
- sudo apt-get install -y python-tk python3-tk
|
|
||||||
82
README.rst
82
README.rst
@@ -2,8 +2,8 @@
|
|||||||
FS-UAE Wrapper
|
FS-UAE Wrapper
|
||||||
==============
|
==============
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/gryf/fs-uae-wrapper.svg?branch=master
|
.. image:: https://github.com/gryf/fs-uae-wrapper/workflows/Test/badge.svg
|
||||||
:target: https://travis-ci.org/gryf/fs-uae-wrapper
|
:target: https://github.com/gryf/fs-uae-wrapper/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/fs-uae-wrapper.svg
|
.. image:: https://img.shields.io/pypi/v/fs-uae-wrapper.svg
|
||||||
:target: https://pypi.python.org/pypi/fs-uae-wrapper
|
:target: https://pypi.python.org/pypi/fs-uae-wrapper
|
||||||
@@ -137,6 +137,7 @@ Currently, couple of wrapper modules are available:
|
|||||||
- cd32
|
- cd32
|
||||||
- archive
|
- archive
|
||||||
- savestate
|
- savestate
|
||||||
|
- whdload
|
||||||
|
|
||||||
plain
|
plain
|
||||||
-----
|
-----
|
||||||
@@ -323,11 +324,13 @@ Options used:
|
|||||||
* ``wrapper_whdload_base`` (required) path to the whdload base system. Usually
|
* ``wrapper_whdload_base`` (required) path to the whdload base system. Usually
|
||||||
it's minimal system containing at least whdload executables in C, and config
|
it's minimal system containing at least whdload executables in C, and config
|
||||||
in S. Read on below for further details.
|
in S. Read on below for further details.
|
||||||
|
* ``wrapper_whdload_options`` (optional) this option will replace the line in
|
||||||
|
``s:whdload-startup`` with specific ``whdload`` options for certain slave.
|
||||||
|
For reference look at WHDLoad documentation and/or on ``s:WHDLoad.prefs``.
|
||||||
|
Note, that ``Slave=`` option must not be used.
|
||||||
* ``wrapper_archive`` (optional) path to the whdload archive, defaults to same
|
* ``wrapper_archive`` (optional) path to the whdload archive, defaults to same
|
||||||
name as configuration file with some detected archive extension. Note, that
|
name as configuration file with some detected archive extension. Note, that
|
||||||
name is case sensitive
|
name is case sensitive
|
||||||
* ``wrapper_archiver`` (optional) archiver to use for storage save state -
|
|
||||||
default ``7z``.
|
|
||||||
|
|
||||||
This module is solely used with whdload distributed games (not just whdload
|
This module is solely used with whdload distributed games (not just whdload
|
||||||
slave files, but whole games, which can be found on several places on the
|
slave files, but whole games, which can be found on several places on the
|
||||||
@@ -336,22 +339,74 @@ internet).
|
|||||||
Base image
|
Base image
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
To make it work, first the minimal system archive need to be prepared. There
|
To make it work, first the absolute minimal image need to contain following
|
||||||
are few dependences to be included in such small system:
|
structure:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
.
|
||||||
|
├── C
|
||||||
|
│  ├── DIC
|
||||||
|
│  ├── Execute
|
||||||
|
│  ├── Patcher
|
||||||
|
│  ├── RawDIC
|
||||||
|
│  ├── SetPatch
|
||||||
|
│  ├── WHDLoad
|
||||||
|
│  └── WHDLoadCD32
|
||||||
|
└── S
|
||||||
|
├── startup-sequence
|
||||||
|
└── WHDLoad.prefs
|
||||||
|
|
||||||
|
where the minimum dependences are:
|
||||||
|
|
||||||
|
- ``Excecute`` from your copy of Workbench
|
||||||
- `WHDLoad`_ 18.9
|
- `WHDLoad`_ 18.9
|
||||||
- `uaequit`_
|
|
||||||
- `SetPatch`_ 43.6
|
- `SetPatch`_ 43.6
|
||||||
- ``Excecute``, ``Assign`` and whatever commands you'll be use in scripts from
|
|
||||||
your copy of Workbench
|
and the ``S/startup-sequence`` should at least contain:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
setpatch QUIET
|
||||||
|
|
||||||
|
IF EXISTS S:whdload-startup
|
||||||
|
Execute S:whdload-startup
|
||||||
|
EndIF
|
||||||
|
|
||||||
|
To leverage more pleasant UX, additionally those bits should be installed (or -
|
||||||
|
copied into base image filesystem):
|
||||||
|
|
||||||
|
- ``Assign`` and whatever commands you'll be use in scripts from your copy of
|
||||||
|
Workbench
|
||||||
|
- `uaequit`_ - this will allow to quit emulator, after quiting game
|
||||||
- `kgiconload`_ - tool for reading icon and executing *default tool* with
|
- `kgiconload`_ - tool for reading icon and executing *default tool* with
|
||||||
optionally defined tool types as parameters (in this case: WHDLoad)
|
optionally defined tool types as parameters (in this case: WHDLoad)
|
||||||
- `SKick`_ optionally - for kickstart relocations. Also images of corresponding
|
- `SKick`_ optionally - for kickstart relocations. Also images of corresponding
|
||||||
kickstart ROM images will be needed.
|
kickstart ROM images will be needed.
|
||||||
|
|
||||||
|
|
||||||
|
and then ``s/startup-sequence`` might looks a follows:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
Assign >NIL: ENV: RAM:
|
||||||
|
Assign >NIL: T: RAM:
|
||||||
|
|
||||||
|
setpatch QUIET
|
||||||
|
|
||||||
|
IF EXISTS S:whdload-startup
|
||||||
|
Execute S:whdload-startup
|
||||||
|
EndIF
|
||||||
|
|
||||||
|
C:UAEquit
|
||||||
|
|
||||||
|
Creating base image archive
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Now, the tree for the minimal image could look like that:
|
Now, the tree for the minimal image could look like that:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
.
|
.
|
||||||
├── C
|
├── C
|
||||||
│  ├── Assign
|
│  ├── Assign
|
||||||
@@ -369,9 +424,12 @@ Now, the tree for the minimal image could look like that:
|
|||||||
└── WHDLoad.prefs
|
└── WHDLoad.prefs
|
||||||
|
|
||||||
to use relocation tables you'll need to place ``Kickstarts`` drawer into Devs
|
to use relocation tables you'll need to place ``Kickstarts`` drawer into Devs
|
||||||
drawer, so it'll looks like this:
|
drawer. Also keep in mind, that corresponding kickstart rom images need to be
|
||||||
|
placed there as well, otherwise it may or may not work. Structure looks like
|
||||||
|
this:
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
.
|
.
|
||||||
├── C
|
├── C
|
||||||
│  ├── Assign
|
│  ├── Assign
|
||||||
@@ -421,7 +479,7 @@ tar with different compressions: ``tar Jcf /tmp/base.tar.xz .``, ``tar zcf
|
|||||||
/tmp/base.tgz .``, ``tar jcf /tmp/base.tar.bz2 .``. It should work with all
|
/tmp/base.tgz .``, ``tar jcf /tmp/base.tar.bz2 .``. It should work with all
|
||||||
mentioned at the beginning of this document archivers.
|
mentioned at the beginning of this document archivers.
|
||||||
|
|
||||||
Starting point is in ``S/startup-sequence`` file, where eventually
|
Starting point is in ``S/startup-sequence`` file, where eventually
|
||||||
``S/whdload-startup`` is executed, which will be created by fs-uae-warpper
|
``S/whdload-startup`` is executed, which will be created by fs-uae-warpper
|
||||||
before execution by fs-uae.
|
before execution by fs-uae.
|
||||||
|
|
||||||
@@ -449,7 +507,7 @@ And execution is as usual:
|
|||||||
|
|
||||||
Now, similar to the archive module, it will create temporary directory, unpack
|
Now, similar to the archive module, it will create temporary directory, unpack
|
||||||
base image there, unpack WHDLoad game archive, search for slave file, and
|
base image there, unpack WHDLoad game archive, search for slave file, and
|
||||||
preapre ``s:whdload-startup``, and finally pass all the configuration to
|
prepare ``s:whdload-startup``, and finally pass all the configuration to
|
||||||
fs-uae.
|
fs-uae.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ the temporary one.
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from fs_uae_wrapper import base
|
from fs_uae_wrapper import base, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class Wrapper(base.ArchiveBase):
|
class Wrapper(base.ArchiveBase):
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ Base class for all wrapper modules
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from fs_uae_wrapper import utils
|
from fs_uae_wrapper import path, utils
|
||||||
from fs_uae_wrapper import path
|
|
||||||
|
|
||||||
|
|
||||||
class Base(object):
|
class Base(object):
|
||||||
@@ -46,7 +44,7 @@ class Base(object):
|
|||||||
if not self._validate_options():
|
if not self._validate_options():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.dir = tempfile.mkdtemp()
|
self.dir = tempfile.mkdtemp(prefix='fs-uae-wrapper-')
|
||||||
self._normalize_options()
|
self._normalize_options()
|
||||||
self._set_assets_paths()
|
self._set_assets_paths()
|
||||||
|
|
||||||
@@ -84,7 +82,7 @@ class Base(object):
|
|||||||
"""execute fs-uae"""
|
"""execute fs-uae"""
|
||||||
curdir = os.path.abspath('.')
|
curdir = os.path.abspath('.')
|
||||||
os.chdir(self.dir)
|
os.chdir(self.dir)
|
||||||
utils.run_command(['fs-uae'] + self.fsuae_options.list())
|
utils.run_command(['fs-uae', *self.fsuae_options.list()])
|
||||||
os.chdir(curdir)
|
os.chdir(curdir)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -325,6 +323,6 @@ class ArchiveBase(Base):
|
|||||||
for ext in ('.7z', '.lha', '.lzx', '.zip', '.rar', '.tar', '.tgz',
|
for ext in ('.7z', '.lha', '.lzx', '.zip', '.rar', '.tar', '.tgz',
|
||||||
'.tar.gz', '.tar.bz2', '.tar.xz'):
|
'.tar.gz', '.tar.bz2', '.tar.xz'):
|
||||||
if ((basename + ext).lower() == fname.lower() and
|
if ((basename + ext).lower() == fname.lower() and
|
||||||
basename == os.path.splitext(fname)[0]):
|
basename == os.path.splitext(fname)[0]):
|
||||||
return fname
|
return fname
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
"""
|
"""
|
||||||
File archive classes
|
File archive classes
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from fs_uae_wrapper import path
|
from fs_uae_wrapper import path
|
||||||
|
|
||||||
|
|
||||||
class Archive(object):
|
class Archive(object):
|
||||||
"""Base class for archive support"""
|
"""Base class for archive support"""
|
||||||
ADD = ['a']
|
ADD = ('a',)
|
||||||
EXTRACT = ['x']
|
EXTRACT = ('x',)
|
||||||
ARCH = 'false'
|
ARCH = 'false'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -27,8 +27,8 @@ class Archive(object):
|
|||||||
files = files if files else ['.']
|
files = files if files else ['.']
|
||||||
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
||||||
" ".join(self.ADD), arch_name, " ".join(files))
|
" ".join(self.ADD), arch_name, " ".join(files))
|
||||||
result = subprocess.call([self._compress] + self.ADD + [arch_name]
|
result = subprocess.call([self._compress, *self.ADD, arch_name,
|
||||||
+ files)
|
*files])
|
||||||
if result != 0:
|
if result != 0:
|
||||||
logging.error("Unable to create archive `%s'.", arch_name)
|
logging.error("Unable to create archive `%s'.", arch_name)
|
||||||
return False
|
return False
|
||||||
@@ -44,8 +44,7 @@ class Archive(object):
|
|||||||
|
|
||||||
logging.debug("Calling `%s %s %s'.", self._compress,
|
logging.debug("Calling `%s %s %s'.", self._compress,
|
||||||
" ".join(self.ADD), arch_name)
|
" ".join(self.ADD), arch_name)
|
||||||
result = subprocess.call([self._decompress] + self.EXTRACT +
|
result = subprocess.call([self._decompress, *self.EXTRACT, arch_name])
|
||||||
[arch_name])
|
|
||||||
if result != 0:
|
if result != 0:
|
||||||
logging.error("Unable to extract archive `%s'.", arch_name)
|
logging.error("Unable to extract archive `%s'.", arch_name)
|
||||||
return False
|
return False
|
||||||
@@ -53,16 +52,16 @@ class Archive(object):
|
|||||||
|
|
||||||
|
|
||||||
class TarArchive(Archive):
|
class TarArchive(Archive):
|
||||||
ADD = ['cf']
|
ADD = ('cf',)
|
||||||
EXTRACT = ['xf']
|
EXTRACT = ('xf',)
|
||||||
ARCH = 'tar'
|
ARCH = 'tar'
|
||||||
|
|
||||||
def create(self, arch_name, files=None):
|
def create(self, arch_name, files=None):
|
||||||
files = files if files else sorted(os.listdir('.'))
|
files = files if files else sorted(os.listdir('.'))
|
||||||
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
||||||
" ".join(self.ADD), arch_name, " ".join(files))
|
" ".join(self.ADD), arch_name, " ".join(files))
|
||||||
result = subprocess.call([self._compress] + self.ADD + [arch_name] +
|
result = subprocess.call([self._compress, *self.ADD, arch_name,
|
||||||
files)
|
*files])
|
||||||
if result != 0:
|
if result != 0:
|
||||||
logging.error("Unable to create archive `%s'.", arch_name)
|
logging.error("Unable to create archive `%s'.", arch_name)
|
||||||
return False
|
return False
|
||||||
@@ -70,15 +69,15 @@ class TarArchive(Archive):
|
|||||||
|
|
||||||
|
|
||||||
class TarGzipArchive(TarArchive):
|
class TarGzipArchive(TarArchive):
|
||||||
ADD = ['zcf']
|
ADD = ('zcf',)
|
||||||
|
|
||||||
|
|
||||||
class TarBzip2Archive(TarArchive):
|
class TarBzip2Archive(TarArchive):
|
||||||
ADD = ['jcf']
|
ADD = ('jcf',)
|
||||||
|
|
||||||
|
|
||||||
class TarXzArchive(TarArchive):
|
class TarXzArchive(TarArchive):
|
||||||
ADD = ['Jcf']
|
ADD = ('Jcf',)
|
||||||
|
|
||||||
|
|
||||||
class LhaArchive(Archive):
|
class LhaArchive(Archive):
|
||||||
@@ -86,8 +85,8 @@ class LhaArchive(Archive):
|
|||||||
|
|
||||||
|
|
||||||
class ZipArchive(Archive):
|
class ZipArchive(Archive):
|
||||||
ADD = ['a', '-tzip']
|
ADD = ('a', '-tzip')
|
||||||
ARCH = ['7z', 'zip']
|
ARCH = ('7z', 'zip')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(ZipArchive, self).__init__()
|
super(ZipArchive, self).__init__()
|
||||||
@@ -102,7 +101,7 @@ class SevenZArchive(Archive):
|
|||||||
|
|
||||||
|
|
||||||
class LzxArchive(Archive):
|
class LzxArchive(Archive):
|
||||||
EXTRACT = ['-x']
|
EXTRACT = ('-x',)
|
||||||
ARCH = 'unlzx'
|
ARCH = 'unlzx'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -113,7 +112,7 @@ class LzxArchive(Archive):
|
|||||||
|
|
||||||
|
|
||||||
class RarArchive(Archive):
|
class RarArchive(Archive):
|
||||||
ARCH = ['rar', 'unrar']
|
ARCH = ('rar', 'unrar')
|
||||||
|
|
||||||
def create(self, arch_name, files=None):
|
def create(self, arch_name, files=None):
|
||||||
files = files if files else sorted(os.listdir('.'))
|
files = files if files else sorted(os.listdir('.'))
|
||||||
@@ -124,8 +123,8 @@ class RarArchive(Archive):
|
|||||||
|
|
||||||
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
logging.debug("Calling `%s %s %s %s'.", self._compress,
|
||||||
" ".join(self.ADD), arch_name, " ".join(files))
|
" ".join(self.ADD), arch_name, " ".join(files))
|
||||||
result = subprocess.call([self._compress] + self.ADD + [arch_name] +
|
result = subprocess.call([self._compress, *self.ADD, arch_name,
|
||||||
files)
|
*files])
|
||||||
if result != 0:
|
if result != 0:
|
||||||
logging.error("Unable to create archive `%s'.", arch_name)
|
logging.error("Unable to create archive `%s'.", arch_name)
|
||||||
return False
|
return False
|
||||||
@@ -134,7 +133,7 @@ class RarArchive(Archive):
|
|||||||
|
|
||||||
class Archivers(object):
|
class Archivers(object):
|
||||||
"""Archivers class"""
|
"""Archivers class"""
|
||||||
archivers = [{'arch': TarArchive, 'name': 'tar', 'ext': ['tar']},
|
archivers = ({'arch': TarArchive, 'name': 'tar', 'ext': ['tar']},
|
||||||
{'arch': TarGzipArchive, 'name': 'tgz',
|
{'arch': TarGzipArchive, 'name': 'tgz',
|
||||||
'ext': ['tar.gz', 'tgz']},
|
'ext': ['tar.gz', 'tgz']},
|
||||||
{'arch': TarBzip2Archive, 'name': 'tar.bz2',
|
{'arch': TarBzip2Archive, 'name': 'tar.bz2',
|
||||||
@@ -144,7 +143,7 @@ class Archivers(object):
|
|||||||
{'arch': SevenZArchive, 'name': '7z', 'ext': ['7z']},
|
{'arch': SevenZArchive, 'name': '7z', 'ext': ['7z']},
|
||||||
{'arch': ZipArchive, 'name': 'zip', 'ext': ['zip']},
|
{'arch': ZipArchive, 'name': 'zip', 'ext': ['zip']},
|
||||||
{'arch': LhaArchive, 'name': 'lha', 'ext': ['lha', 'lzh']},
|
{'arch': LhaArchive, 'name': 'lha', 'ext': ['lha', 'lzh']},
|
||||||
{'arch': LzxArchive, 'name': 'lzx', 'ext': ['lzx']}]
|
{'arch': LzxArchive, 'name': 'lzx', 'ext': ['lzx']})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, extension):
|
def get(cls, extension):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MessageGui(tkinter.Tk):
|
|||||||
# Display window without decorations
|
# Display window without decorations
|
||||||
self.wm_attributes('-type', 'splash')
|
self.wm_attributes('-type', 'splash')
|
||||||
|
|
||||||
self.frame = ttk.Frame(self, padding=5, borderwidth=0)
|
self.frame = tkinter.ttk.Frame(self, padding=5, borderwidth=0)
|
||||||
self.frame.grid()
|
self.frame.grid()
|
||||||
tkinter.ttk.Label(self.frame, text=msg, relief="ridge",
|
tkinter.ttk.Label(self.frame, text=msg, relief="ridge",
|
||||||
padding=10).grid()
|
padding=10).grid()
|
||||||
|
|||||||
18
fs_uae_wrapper/nogui_message.py
Normal file
18
fs_uae_wrapper/nogui_message.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Display message as simple text on console
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
"""Just a fake message window for systems without TK"""
|
||||||
|
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
self._process = None
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
sys.stdout.write(self.msg + "\n")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
Simple class for executing fs-uae with specified parameters. This is a
|
Simple class for executing fs-uae with specified parameters. This is a
|
||||||
failsafe class for running fs-uae.
|
failsafe class for running fs-uae.
|
||||||
"""
|
"""
|
||||||
from fs_uae_wrapper import base
|
from fs_uae_wrapper import base, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class Wrapper(base.Base):
|
class Wrapper(base.Base):
|
||||||
@@ -17,8 +16,8 @@ class Wrapper(base.Base):
|
|||||||
|
|
||||||
def _run_emulator(self):
|
def _run_emulator(self):
|
||||||
"""execute fs-uae"""
|
"""execute fs-uae"""
|
||||||
utils.run_command(['fs-uae'] + [self.conf_file] +
|
utils.run_command(['fs-uae', self.conf_file,
|
||||||
self.fsuae_options.list())
|
*self.fsuae_options.list()])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Do the cleanup. Here - just do nothing"""
|
"""Do the cleanup. Here - just do nothing"""
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import pathlib
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from fs_uae_wrapper import message
|
|
||||||
from fs_uae_wrapper import file_archive
|
from fs_uae_wrapper import file_archive
|
||||||
|
try:
|
||||||
|
from fs_uae_wrapper.message import Message
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
from fs_uae_wrapper.nogui_message import Message
|
||||||
|
|
||||||
|
|
||||||
class CmdOption(dict):
|
class CmdOption(dict):
|
||||||
@@ -65,7 +68,7 @@ def operate_archive(arch_name, operation, text, params):
|
|||||||
if archiver is None:
|
if archiver is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
msg = message.Message(text)
|
msg = Message(text)
|
||||||
if text:
|
if text:
|
||||||
msg.show()
|
msg.show()
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ It will use compressed base image and compressed directories.
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
from fs_uae_wrapper import base
|
from fs_uae_wrapper import base, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class Wrapper(base.ArchiveBase):
|
class Wrapper(base.ArchiveBase):
|
||||||
@@ -62,7 +60,6 @@ class Wrapper(base.ArchiveBase):
|
|||||||
"location.", base_image)
|
"location.", base_image)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
title = self._get_title()
|
|
||||||
curdir = os.path.abspath('.')
|
curdir = os.path.abspath('.')
|
||||||
os.chdir(self.dir)
|
os.chdir(self.dir)
|
||||||
result = utils.extract_archive(base_image)
|
result = utils.extract_archive(base_image)
|
||||||
@@ -83,12 +80,20 @@ class Wrapper(base.ArchiveBase):
|
|||||||
# find slave name
|
# find slave name
|
||||||
slave_fname = None
|
slave_fname = None
|
||||||
slave_path = None
|
slave_path = None
|
||||||
|
case_insensitvie_map = {}
|
||||||
|
|
||||||
|
# build case insensitive map of paths and find the slave file
|
||||||
|
for root, dirnames, fnames in os.walk('.'):
|
||||||
|
for dirname in dirnames:
|
||||||
|
full_path = os.path.normpath(os.path.join(root, dirname))
|
||||||
|
case_insensitvie_map[full_path.lower()] = full_path
|
||||||
|
|
||||||
for root, dirs, fnames in os.walk('.'):
|
|
||||||
for fname in fnames:
|
for fname in fnames:
|
||||||
if fname.lower().endswith('.slave'):
|
full_path = os.path.normpath(os.path.join(root, fname))
|
||||||
|
case_insensitvie_map[full_path.lower()] = full_path
|
||||||
|
if not slave_fname and fname.lower().endswith('.slave'):
|
||||||
slave_path, slave_fname = os.path.normpath(root), fname
|
slave_path, slave_fname = os.path.normpath(root), fname
|
||||||
break
|
|
||||||
if slave_fname is None:
|
if slave_fname is None:
|
||||||
logging.error("Cannot find .slave file in archive.")
|
logging.error("Cannot find .slave file in archive.")
|
||||||
return False
|
return False
|
||||||
@@ -97,8 +102,8 @@ class Wrapper(base.ArchiveBase):
|
|||||||
icon_fname = None
|
icon_fname = None
|
||||||
for fname in os.listdir(slave_path):
|
for fname in os.listdir(slave_path):
|
||||||
if (fname.lower().endswith('.info') and
|
if (fname.lower().endswith('.info') and
|
||||||
os.path.splitext(slave_fname)[0].lower() ==
|
os.path.splitext(slave_fname)[0].lower() ==
|
||||||
os.path.splitext(fname)[0].lower()):
|
os.path.splitext(fname)[0].lower()):
|
||||||
icon_fname = fname
|
icon_fname = fname
|
||||||
break
|
break
|
||||||
if icon_fname is None:
|
if icon_fname is None:
|
||||||
@@ -106,10 +111,26 @@ class Wrapper(base.ArchiveBase):
|
|||||||
"archive.", slave_fname)
|
"archive.", slave_fname)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Write startup file
|
# find proper way to handle slave
|
||||||
with open("S/whdload-startup", "w") as fobj:
|
# 1. check if there are user provided params
|
||||||
fobj.write(f"cd {slave_path}\n")
|
contents = f"cd {slave_path}\n"
|
||||||
fobj.write(f"C:kgiconload {icon_fname}\n")
|
if self.all_options.get('wrapper_whdload_options'):
|
||||||
|
contents = (f"{contents}"
|
||||||
|
f"C:whdload "
|
||||||
|
f"{self.all_options['wrapper_whdload_options']} "
|
||||||
|
f"Slave={slave_fname}\n")
|
||||||
|
else:
|
||||||
|
# no params, find if kgiconload is available
|
||||||
|
if case_insensitvie_map.get('c/kgiconload'):
|
||||||
|
contents = f"{contents}C:kgiconload {icon_fname}\n"
|
||||||
|
else:
|
||||||
|
# if not, just add common defaults
|
||||||
|
contents = (f"{contents}C:whdload Preload "
|
||||||
|
f"Slave={slave_fname}\n")
|
||||||
|
|
||||||
|
fname = os.path.join(case_insensitvie_map.get('s'), 'whdload-startup')
|
||||||
|
with open(fname, "w") as fobj:
|
||||||
|
fobj.write(contents)
|
||||||
|
|
||||||
os.chdir(curdir)
|
os.chdir(curdir)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from fs_uae_wrapper import utils
|
from fs_uae_wrapper import WRAPPER_KEY, utils
|
||||||
from fs_uae_wrapper import WRAPPER_KEY
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(options):
|
def setup_logger(options):
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools >= 61.0"]
|
requires = ["setuptools >= 77.0", "wheel", "setuptools-git-versioning"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "fs-uae-wrapper"
|
name = "fs-uae-wrapper"
|
||||||
authors = [{name = "Roman Dobosz", email = "gryf73@gmail.com"}]
|
authors = [{name = "Roman Dobosz", email = "gryf73@gmail.com"}]
|
||||||
license = {text = "BSD"}
|
license = "BSD-3-Clause"
|
||||||
description = "Automate archives support and state saves for fs-uae"
|
description = "Automate archives support and state saves for fs-uae"
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
keywords = ["uae", "fs-uae", "amiga", "emulator", "wrapper"]
|
keywords = ["uae", "fs-uae", "amiga", "emulator", "wrapper"]
|
||||||
version = "0.10.0"
|
dynamic = ["version"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Intended Audience :: End Users/Desktop",
|
"Intended Audience :: End Users/Desktop",
|
||||||
"License :: OSI Approved :: BSD License",
|
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
@@ -34,7 +33,18 @@ Homepage = "https://github.com/gryf/fs-uae-wrapper"
|
|||||||
fs-uae-wrapper = "fs_uae_wrapper.wrapper:run"
|
fs-uae-wrapper = "fs_uae_wrapper.wrapper:run"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
py-modules = ["fs_uae_wrapper"]
|
packages = ["fs_uae_wrapper"]
|
||||||
|
|
||||||
[tool.distutils.bdist_wheel]
|
[tool.distutils.bdist_wheel]
|
||||||
universal = true
|
universal = true
|
||||||
|
|
||||||
|
[tool.setuptools-git-versioning]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"F", # pyflakes
|
||||||
|
"E", # pycodestyle
|
||||||
|
"I", # isort
|
||||||
|
"RUF", # ruff-specific rules
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
pytest-pep8
|
|
||||||
coverage
|
|
||||||
flake8
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import archive
|
from fs_uae_wrapper import archive, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestArchive(TestCase):
|
class TestArchive(TestCase):
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkstemp, mkdtemp
|
import sys
|
||||||
from unittest import TestCase
|
from tempfile import mkdtemp, mkstemp
|
||||||
from unittest import mock
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from fs_uae_wrapper import base
|
from fs_uae_wrapper import base, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestBase(TestCase):
|
class TestBase(TestCase):
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import cd32
|
from fs_uae_wrapper import cd32, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestCD32(TestCase):
|
class TestCD32(TestCase):
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import file_archive
|
from fs_uae_wrapper import file_archive
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from unittest import TestCase
|
|
||||||
import os
|
import os
|
||||||
from unittest import mock
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from fs_uae_wrapper import message
|
from fs_uae_wrapper import message
|
||||||
|
from fs_uae_wrapper import nogui_message
|
||||||
|
|
||||||
if os.environ.get('DISPLAY'):
|
if os.environ.get('DISPLAY'):
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -39,6 +39,19 @@ class TestMessage(TestCase):
|
|||||||
msg._process.join.assert_called_once()
|
msg._process.join.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestNOPMessage(TestCase):
|
||||||
|
|
||||||
|
@mock.patch('sys.stdout.write')
|
||||||
|
def test_show(self, stdout_write):
|
||||||
|
msg = nogui_message.Message('display that')
|
||||||
|
msg.show()
|
||||||
|
stdout_write.assert_called_once()
|
||||||
|
|
||||||
|
def test_close(self):
|
||||||
|
msg = nogui_message.Message('display that')
|
||||||
|
self.assertIsNone(msg.close())
|
||||||
|
|
||||||
|
|
||||||
if os.environ.get('DISPLAY'):
|
if os.environ.get('DISPLAY'):
|
||||||
# Tkinter needs graphic environment for the widgets
|
# Tkinter needs graphic environment for the widgets
|
||||||
class TestSpawn(TestCase):
|
class TestSpawn(TestCase):
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import plain
|
from fs_uae_wrapper import plain, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestPlainModule(TestCase):
|
class TestPlainModule(TestCase):
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import savestate
|
from fs_uae_wrapper import savestate, utils
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestSaveState(TestCase):
|
class TestSaveState(TestCase):
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from tempfile import mkstemp, mkdtemp
|
|
||||||
from unittest import TestCase
|
|
||||||
import shutil
|
import shutil
|
||||||
from unittest import mock
|
import sys
|
||||||
|
from tempfile import mkdtemp, mkstemp
|
||||||
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from fs_uae_wrapper import utils
|
from fs_uae_wrapper import utils
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from unittest import TestCase
|
from unittest import TestCase, mock
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from fs_uae_wrapper import whdload
|
from fs_uae_wrapper import utils, whdload
|
||||||
from fs_uae_wrapper import utils
|
|
||||||
|
|
||||||
|
|
||||||
class TestWHDLoad(TestCase):
|
class TestWHDLoad(TestCase):
|
||||||
@@ -163,7 +161,7 @@ class TestWHDLoad(TestCase):
|
|||||||
@mock.patch('os.chdir')
|
@mock.patch('os.chdir')
|
||||||
def test_find_slave_no_slave_file(self, chdir, walk):
|
def test_find_slave_no_slave_file(self, chdir, walk):
|
||||||
walk.return_value = [(".", ('game'), ()),
|
walk.return_value = [(".", ('game'), ()),
|
||||||
('./game', (), ('foo', 'bar', 'baz'))]
|
('./game', (), ('foo', 'bar', 'baz'))]
|
||||||
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
||||||
self.assertFalse(wrapper._find_slave())
|
self.assertFalse(wrapper._find_slave())
|
||||||
|
|
||||||
@@ -173,20 +171,63 @@ class TestWHDLoad(TestCase):
|
|||||||
def test_find_slave_no_corresponding_icon(self, chdir, walk, listdir):
|
def test_find_slave_no_corresponding_icon(self, chdir, walk, listdir):
|
||||||
contents = ('foo', 'bar', 'baz.slave')
|
contents = ('foo', 'bar', 'baz.slave')
|
||||||
walk.return_value = [(".", ('game'), ()),
|
walk.return_value = [(".", ('game'), ()),
|
||||||
('./game', (), contents)]
|
('./game', (), contents)]
|
||||||
listdir.return_value = contents
|
listdir.return_value = contents
|
||||||
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
||||||
self.assertFalse(wrapper._find_slave())
|
self.assertFalse(wrapper._find_slave())
|
||||||
|
|
||||||
@mock.patch('builtins.open')
|
|
||||||
@mock.patch('os.listdir')
|
@mock.patch('os.listdir')
|
||||||
@mock.patch('os.walk')
|
@mock.patch('os.walk')
|
||||||
@mock.patch('os.chdir')
|
@mock.patch('os.chdir')
|
||||||
def test_find_slave_success(self, chdir, walk, listdir, bopen):
|
def test_find_slave_success(self, chdir, walk, listdir):
|
||||||
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
|
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
|
||||||
walk.return_value = [(".", ('game'), ()),
|
_open = mock.mock_open()
|
||||||
('./game', (), contents)]
|
walk.return_value = [(".", ('C', 'S', 'game'), ()),
|
||||||
|
('./C', (), ('Assign', 'kgiconload')),
|
||||||
|
('./S', (), ()),
|
||||||
|
('./game', (), contents)]
|
||||||
listdir.return_value = contents
|
listdir.return_value = contents
|
||||||
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
||||||
self.assertTrue(wrapper._find_slave())
|
with mock.patch('builtins.open', _open):
|
||||||
bopen.assert_called_once()
|
self.assertTrue(wrapper._find_slave())
|
||||||
|
handle = _open()
|
||||||
|
handle.write.assert_called_once_with('cd game\n'
|
||||||
|
'C:kgiconload baz.info\n')
|
||||||
|
|
||||||
|
@mock.patch('os.listdir')
|
||||||
|
@mock.patch('os.walk')
|
||||||
|
@mock.patch('os.chdir')
|
||||||
|
def test_find_slave_minial(self, chdir, walk, listdir):
|
||||||
|
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
|
||||||
|
_open = mock.mock_open()
|
||||||
|
walk.return_value = [(".", ('C', 'S', 'game'), ()),
|
||||||
|
('./C', (), ('Assign', 'WHDLoad')),
|
||||||
|
('./S', (), ()),
|
||||||
|
('./game', (), contents)]
|
||||||
|
listdir.return_value = contents
|
||||||
|
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
||||||
|
with mock.patch('builtins.open', _open):
|
||||||
|
self.assertTrue(wrapper._find_slave())
|
||||||
|
handle = _open()
|
||||||
|
handle.write.assert_called_once_with('cd game\nC:whdload Preload '
|
||||||
|
'Slave=baz.slave\n')
|
||||||
|
|
||||||
|
@mock.patch('os.listdir')
|
||||||
|
@mock.patch('os.walk')
|
||||||
|
@mock.patch('os.chdir')
|
||||||
|
def test_find_custom_options(self, chdir, walk, listdir):
|
||||||
|
contents = ('foo', 'bar', 'baz.slave', 'baz.info')
|
||||||
|
_open = mock.mock_open()
|
||||||
|
walk.return_value = [(".", ('C', 'S', 'game'), ()),
|
||||||
|
('./C', (), ('Assign', 'WHDLoad')),
|
||||||
|
('./S', (), ()),
|
||||||
|
('./game', (), contents)]
|
||||||
|
listdir.return_value = contents
|
||||||
|
wrapper = whdload.Wrapper('Config.fs-uae', utils.CmdOption(), {})
|
||||||
|
whdl_opts = 'Preload SplashDelay=0 MMU PAL'
|
||||||
|
wrapper.all_options['wrapper_whdload_options'] = whdl_opts
|
||||||
|
with mock.patch('builtins.open', _open):
|
||||||
|
self.assertTrue(wrapper._find_slave())
|
||||||
|
handle = _open()
|
||||||
|
handle.write.assert_called_once_with(f'cd game\nC:whdload {whdl_opts} '
|
||||||
|
'Slave=baz.slave\n')
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from tempfile import mkstemp, mkdtemp
|
|
||||||
from unittest import TestCase
|
|
||||||
import shutil
|
import shutil
|
||||||
from unittest import mock
|
import sys
|
||||||
|
from tempfile import mkdtemp, mkstemp
|
||||||
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from fs_uae_wrapper import wrapper
|
from fs_uae_wrapper import wrapper
|
||||||
|
|
||||||
|
|||||||
7
tox.ini
7
tox.ini
@@ -8,7 +8,12 @@ usedevelop=True
|
|||||||
setenv = COVERAGE_FILE = .coverage
|
setenv = COVERAGE_FILE = .coverage
|
||||||
commands = py.test --cov=fs_uae_wrapper --cov-report=term-missing
|
commands = py.test --cov=fs_uae_wrapper --cov-report=term-missing
|
||||||
|
|
||||||
deps = -r{toxinidir}/test-requirements.txt
|
deps =
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-pep8
|
||||||
|
coverage
|
||||||
|
flake8
|
||||||
|
|
||||||
[testenv:py3-flake8]
|
[testenv:py3-flake8]
|
||||||
basepython = python3
|
basepython = python3
|
||||||
|
|||||||
Reference in New Issue
Block a user