Skip to content

Commit

Permalink
Merge branch 'release/2.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
wolph committed Jul 9, 2022
2 parents 232c391 + ec5a037 commit 2644943
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 149 deletions.
44 changes: 36 additions & 8 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,52 @@ on:
branches: [ develop ]

jobs:
build:
# Run os specific tests on the slower OS X/Windows machines
windows_osx:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.9', '3.10']
os: ['macos-latest', 'windows-latest']

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Python version
run: python --version
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install -e ".[tests]"
- name: Test with pytest
run: python -m pytest

# Run all tests including Redis on Linux
linux:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ['3.8', '3.9', '3.10']

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Start Redis
uses: supercharge/redis-github-action@1.4.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Python version
run: python --version
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install tox
- name: Test with tox
run: |
tox
- name: Test with pytest
run: tox -p all
7 changes: 0 additions & 7 deletions msvcrt.pyi

This file was deleted.

2 changes: 1 addition & 1 deletion portalocker/__about__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__package_name__ = 'portalocker'
__author__ = 'Rick van Hattem'
__email__ = 'wolph@wol.ph'
__version__ = '2.4.0'
__version__ = '2.5.0'
__description__ = '''Wraps the portalocker recipe for easy usage'''
__url__ = 'https://github.com/WoLpH/portalocker'

2 changes: 1 addition & 1 deletion portalocker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#: Current author's email address
__email__ = __about__.__email__
#: Version number
__version__ = '2.4.0'
__version__ = '2.5.0'
#: Package description for Pypi
__description__ = __about__.__description__
#: Package homepage
Expand Down
4 changes: 2 additions & 2 deletions portalocker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ class LockException(BaseLockException):
pass


class AlreadyLocked(BaseLockException):
class AlreadyLocked(LockException):
pass


class FileToLarge(BaseLockException):
class FileToLarge(LockException):
pass
142 changes: 50 additions & 92 deletions portalocker/portalocker.py
Original file line number Diff line number Diff line change
@@ -1,132 +1,90 @@
import os
import sys

import typing

from . import constants
from . import exceptions

if os.name == 'nt': # pragma: no cover
import msvcrt
import pywintypes
import win32con
import win32file
import pywintypes
import winerror
import msvcrt

__overlapped = pywintypes.OVERLAPPED()

if sys.version_info.major == 2:
lock_length = -1
else:
lock_length = int(2**31 - 1)

def lock(file_: typing.IO, flags: constants.LockFlags):
if flags & constants.LockFlags.SHARED:
if sys.version_info.major == 2:
if flags & constants.LockFlags.NON_BLOCKING:
mode = win32con.LOCKFILE_FAIL_IMMEDIATELY
else:
mode = 0
mode = 0
if flags & constants.LockFlags.NON_BLOCKING:
mode |= win32con.LOCKFILE_FAIL_IMMEDIATELY

else:
if flags & constants.LockFlags.NON_BLOCKING:
mode = msvcrt.LK_NBRLCK
else:
mode = msvcrt.LK_RLCK
if flags & constants.LockFlags.EXCLUSIVE:
mode |= win32con.LOCKFILE_EXCLUSIVE_LOCK

# is there any reason not to reuse the following structure?
hfile = win32file._get_osfhandle(file_.fileno())
try:
win32file.LockFileEx(hfile, mode, 0, -0x10000, __overlapped)
except pywintypes.error as exc_value:
# error: (33, 'LockFileEx', 'The process cannot access the file
# because another process has locked a portion of the file.')
if exc_value.winerror == winerror.ERROR_LOCK_VIOLATION:
raise exceptions.LockException(
exceptions.LockException.LOCK_FAILED,
exc_value.strerror,
fh=file_)
else:
# Q: Are there exceptions/codes we should be dealing with
# here?
raise
else:
if flags & constants.LockFlags.NON_BLOCKING:
mode = msvcrt.LK_NBLCK
# Save the old position so we can go back to that position but
# still lock from the beginning of the file
savepos = file_.tell()
if savepos:
file_.seek(0)

os_fh = msvcrt.get_osfhandle(file_.fileno())
try:
win32file.LockFileEx(os_fh, mode, 0, -0x10000, __overlapped)
except pywintypes.error as exc_value:
# error: (33, 'LockFileEx', 'The process cannot access the file
# because another process has locked a portion of the file.')
if exc_value.winerror == winerror.ERROR_LOCK_VIOLATION:
raise exceptions.AlreadyLocked(
exceptions.LockException.LOCK_FAILED,
exc_value.strerror,
fh=file_
)
else:
mode = msvcrt.LK_LOCK
# Q: Are there exceptions/codes we should be dealing with
# here?
raise
finally:
if savepos:
file_.seek(savepos)

# windows locks byte ranges, so make sure to lock from file start
try:
savepos = file_.tell()
if savepos:
# [ ] test exclusive lock fails on seek here
# [ ] test if shared lock passes this point
file_.seek(0)
# [x] check if 0 param locks entire file (not documented in
# Python)
# [x] fails with "IOError: [Errno 13] Permission denied",
# but -1 seems to do the trick

try:
msvcrt.locking(file_.fileno(), mode, lock_length)
except IOError as exc_value:
# [ ] be more specific here
raise exceptions.LockException(
exceptions.LockException.LOCK_FAILED,
exc_value.strerror,
fh=file_)
finally:
if savepos:
file_.seek(savepos)
except IOError as exc_value:
raise exceptions.LockException(
exceptions.LockException.LOCK_FAILED, exc_value.strerror,
fh=file_)

def unlock(file_: typing.IO):
try:
savepos = file_.tell()
if savepos:
file_.seek(0)

os_fh = msvcrt.get_osfhandle(file_.fileno())
try:
msvcrt.locking(file_.fileno(), constants.LockFlags.UNBLOCK,
lock_length)
except IOError as exc:
exception = exc
if exc.strerror == 'Permission denied':
hfile = win32file._get_osfhandle(file_.fileno())
try:
win32file.UnlockFileEx(
hfile, 0, -0x10000, __overlapped)
except pywintypes.error as exc:
exception = exc
if exc.winerror == winerror.ERROR_NOT_LOCKED:
# error: (158, 'UnlockFileEx',
# 'The segment is already unlocked.')
# To match the 'posix' implementation, silently
# ignore this error
pass
else:
# Q: Are there exceptions/codes we should be
# dealing with here?
raise
win32file.UnlockFileEx(
os_fh, 0, -0x10000, __overlapped
)
except pywintypes.error as exc:
if exc.winerror == winerror.ERROR_NOT_LOCKED:
# error: (158, 'UnlockFileEx',
# 'The segment is already unlocked.')
# To match the 'posix' implementation, silently
# ignore this error
pass
else:
raise exceptions.LockException(
exceptions.LockException.LOCK_FAILED,
exception.strerror,
fh=file_)
# Q: Are there exceptions/codes we should be
# dealing with here?
raise
finally:
if savepos:
file_.seek(savepos)
except IOError as exc:
raise exceptions.LockException(
exceptions.LockException.LOCK_FAILED, exc.strerror,
fh=file_)
fh=file_
)

elif os.name == 'posix': # pragma: no cover
import fcntl


def lock(file_: typing.IO, flags: constants.LockFlags):
locking_exceptions = IOError,
try: # pragma: no cover
Expand All @@ -141,9 +99,9 @@ def lock(file_: typing.IO, flags: constants.LockFlags):
# every IO error
raise exceptions.LockException(exc_value, fh=file_)


def unlock(file_: typing.IO, ):
fcntl.flock(file_.fileno(), constants.LockFlags.UNBLOCK)

else: # pragma: no cover
raise RuntimeError('PortaLocker only defined for nt and posix platforms')

20 changes: 13 additions & 7 deletions portalocker_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import py
import logging
import pytest

import random
import multiprocessing

logger = logging.getLogger(__name__)


@pytest.fixture
def tmpfile(tmpdir_factory):
tmpdir = tmpdir_factory.mktemp('temp')
filename = tmpdir.join('tmpfile')
def tmpfile(tmp_path):
filename = tmp_path / str(random.random())
yield str(filename)
try:
filename.remove(ignore_errors=True)
except (py.error.EBUSY, py.error.ENOENT):
filename.unlink(missing_ok=True)
except PermissionError:
pass


def pytest_sessionstart(session):
# Force spawning the process so we don't accidently inherit locks.
# I'm not a 100% certain this will work correctly unfortunately... there
# is some potential for breaking tests
multiprocessing.set_start_method('spawn')

0 comments on commit 2644943

Please sign in to comment.